From 16221e755038a12079c2ff8029ff53a628c24bc3 Mon Sep 17 00:00:00 2001 From: Suriya Subramanian Date: Sat, 17 Jul 2021 15:54:23 +0000 Subject: [PATCH 001/111] Fix handling of gzip-encoded text response When Zappa receives a compressed text/plain response from the application, it tries to process it as a text response. Instead, Zappa should treat the response as if it were a binary one and base-64 encode the response body. See issue #2080 binary_support logic in handler.py (0.51.0) broke compressed text response https://github.com/Miserlou/Zappa/issues/2080 --- zappa/handler.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/zappa/handler.py b/zappa/handler.py index 41336dc15..b20f45ef4 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -583,7 +583,16 @@ def handler(self, event, context): ) if response.data: - if ( + if settings.BINARY_SUPPORT and response.headers.get( + "Content-Encoding" + ): + # We could have a text response that's gzip + # encoded. Therefore, we base-64 encode it. + zappa_returndict["body"] = base64.b64encode( + response.data + ).decode("utf-8") + zappa_returndict["isBase64Encoded"] = True + elif ( settings.BINARY_SUPPORT and not response.mimetype.startswith("text/") and response.mimetype != "application/json" From b4d80dfe5f4eb4427c0e05878da961e39ac8dbae Mon Sep 17 00:00:00 2001 From: Yann Pretot Date: Wed, 10 Nov 2021 21:41:53 +0100 Subject: [PATCH 002/111] Move from CNAME to Alias Route53 records --- tests/tests.py | 2 +- zappa/core.py | 34 +++++++++------------------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index e89bb4dd6..a61b9e9f3 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1858,7 +1858,7 @@ def test_get_domain_respects_route53_setting(self, client, template): "HostedZones": [{"Id": "somezone"}], } zappa_core.route53.list_resource_record_sets.return_value = { - "ResourceRecordSets": [{"Type": "CNAME", "Name": "test_domain1"}] + "ResourceRecordSets": [{"Type": "A", "Name": "test_domain1"}] } record = zappa_core.get_domain_name("test_domain") diff --git a/zappa/core.py b/zappa/core.py index 0a28e33ba..6d6975589 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2655,36 +2655,20 @@ def update_route53_records(self, domain_name, dns_name): """ zone_id = self.get_hosted_zone_id_for_domain(domain_name) - is_apex = ( - self.route53.get_hosted_zone(Id=zone_id)["HostedZone"]["Name"][:-1] - == domain_name - ) - if is_apex: - record_set = { - "Name": domain_name, - "Type": "A", - "AliasTarget": { - "HostedZoneId": "Z2FDTNDATAQYW2", # This is a magic value that means "CloudFront" - "DNSName": dns_name, - "EvaluateTargetHealth": False, - }, - } - else: - record_set = { - "Name": domain_name, - "Type": "CNAME", - "ResourceRecords": [{"Value": dns_name}], - "TTL": 60, - } + record_set = { + "Name": domain_name, + "Type": "A", + "AliasTarget": { + "HostedZoneId": "Z2FDTNDATAQYW2", # This is a magic value that means "CloudFront" + "DNSName": dns_name, + "EvaluateTargetHealth": False, + }, + } # Related: https://github.com/boto/boto3/issues/157 # and: http://docs.aws.amazon.com/Route53/latest/APIReference/CreateAliasRRSAPI.html # and policy: https://spin.atomicobject.com/2016/04/28/route-53-hosted-zone-managment/ - # pure_zone_id = zone_id.split('/hostedzone/')[1] - # XXX: ClientError: An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: - # Tried to create an alias that targets d1awfeji80d0k2.cloudfront.net., type A in zone Z1XWOQP59BYF6Z, - # but the alias target name does not lie within the target zone response = self.route53.change_resource_record_sets( HostedZoneId=zone_id, ChangeBatch={ From 8a53365c0d9895a68efb21b4a0e6954f441af069 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 24 Nov 2021 12:37:12 +0800 Subject: [PATCH 003/111] Update asynchronous.py --- zappa/asynchronous.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zappa/asynchronous.py b/zappa/asynchronous.py index 7ace97332..cb197cfc7 100644 --- a/zappa/asynchronous.py +++ b/zappa/asynchronous.py @@ -187,7 +187,7 @@ def _send(self, message): Given a message, directly invoke the lamdba function for this task. """ message["command"] = "zappa.asynchronous.route_lambda_task" - payload = json.dumps(message).encode("utf-8") + payload = json.dumps(message) if len(payload) > LAMBDA_ASYNC_PAYLOAD_LIMIT: # pragma: no cover raise AsyncException("Payload too large for async Lambda call") self.response = self.client.invoke( @@ -259,7 +259,7 @@ def _send(self, message): Given a message, publish to this topic. """ message["command"] = "zappa.asynchronous.route_sns_task" - payload = json.dumps(message).encode("utf-8") + payload = json.dumps(message) if len(payload) > LAMBDA_ASYNC_PAYLOAD_LIMIT: # pragma: no cover raise AsyncException("Payload too large for SNS") self.response = self.client.publish(TargetArn=self.arn, Message=payload) @@ -309,6 +309,7 @@ def run_message(message): "ttl": {"N": str(int(time.time() + 600))}, "async_status": {"S": "in progress"}, "async_response": {"S": str(json.dumps("N/A"))}, + "message": {"S": json.dumps(message)}, }, ) From 8d4c9f2b683698bd261ac26cca10c2fb5dde6465 Mon Sep 17 00:00:00 2001 From: DS992 <> Date: Thu, 14 Apr 2022 22:02:11 +0200 Subject: [PATCH 004/111] Add Support for Graviton 2 / ARM Architecture --- README.md | 1 + zappa/cli.py | 11 ++++++++++- zappa/core.py | 19 +++++++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ae4bcae0f..f2fd3f34d 100644 --- a/README.md +++ b/README.md @@ -833,6 +833,7 @@ to change Zappa's behavior. Use these at your own risk! "assume_policy": "my_assume_policy.json", // optional, IAM assume policy JSON file "attach_policy": "my_attach_policy.json", // optional, IAM attach policy JSON file "apigateway_policy": "my_apigateway_policy.json", // optional, API Gateway resource policy JSON file + "architectures": "x86_64", // optional, Set Lambda Architecture, defaults to x86_64. For Graviton 2 use: arm64 "async_source": "sns", // Source of async tasks. Defaults to "lambda" "async_resources": true, // Create the SNS topic and DynamoDB table to use. Defaults to true. "async_response_table": "your_dynamodb_table_name", // the DynamoDB table name to use for captured async responses; defaults to None (can't capture) diff --git a/zappa/cli.py b/zappa/cli.py index 546e4b19c..01ac3331a 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -126,6 +126,7 @@ class ZappaCLI: context_header_mappings = None tags = [] layers = None + architecture = None stage_name_env_pattern = re.compile("^[a-zA-Z0-9_]+$") @@ -911,6 +912,7 @@ def deploy(self, source_zip=None, docker_image_uri=None): use_alb=self.use_alb, layers=self.layers, concurrency=self.lambda_concurrency, + architecture=self.architecture, ) kwargs["function_name"] = self.lambda_name if docker_image_uri: @@ -1136,6 +1138,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): function_name=self.lambda_name, num_revisions=self.num_retained_versions, concurrency=self.lambda_concurrency, + architecture=self.architecture, ) if docker_image_uri: kwargs["docker_image_uri"] = docker_image_uri @@ -1172,6 +1175,8 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): aws_kms_key_arn=self.aws_kms_key_arn, layers=self.layers, wait=False, + architecture=self.architecture, + ) # Finally, delete the local copy our zip package @@ -2544,7 +2549,7 @@ def load_settings(self, settings_file=None, session=None): self.num_retained_versions = self.stage_config.get( "num_retained_versions", None ) - + self.architecture = [self.stage_config.get("architecture", "x86_64")] # Check for valid values of num_retained_versions if ( self.num_retained_versions is not None @@ -2615,6 +2620,9 @@ def load_settings(self, settings_file=None, session=None): # Additional tags self.tags = self.stage_config.get("tags", {}) + # Architectures + self.architecture = [self.stage_config.get("architecture", "x86_64")] + desired_role_name = self.lambda_name + "-ZappaLambdaExecutionRole" self.zappa = Zappa( boto_session=session, @@ -2627,6 +2635,7 @@ def load_settings(self, settings_file=None, session=None): tags=self.tags, endpoint_urls=self.stage_config.get("aws_endpoint_urls", {}), xray_tracing=self.xray_tracing, + architecture=self.architecture ) for setting in CUSTOM_SETTINGS: diff --git a/zappa/core.py b/zappa/core.py index 51fa74836..16db6d770 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -264,7 +264,7 @@ class Zappa: apigateway_policy = None cloudwatch_log_levels = ["OFF", "ERROR", "INFO"] xray_tracing = False - + architecture = None ## # Credentials ## @@ -284,6 +284,7 @@ def __init__( tags=(), endpoint_urls={}, xray_tracing=False, + architecture=None ): """ Instantiate this new Zappa instance, loading any custom credentials if necessary. @@ -316,13 +317,17 @@ def __init__( else: self.manylinux_suffix_start = "cp39" + if not self.architecture: + self.architecture = "x86_64" + # AWS Lambda supports manylinux1/2010, manylinux2014, and manylinux_2_24 manylinux_suffixes = ("_2_24", "2014", "2010", "1") self.manylinux_wheel_file_match = re.compile( - f'^.*{self.manylinux_suffix_start}-(manylinux_\d+_\d+_x86_64[.])?manylinux({"|".join(manylinux_suffixes)})_x86_64[.]whl$' + f'^.*{self.manylinux_suffix_start}-(manylinux_\d+_\d+_{self.architecture}[.])?manylinux({"|".join(manylinux_suffixes)})_{self.architecture}[.]whl$' ) self.manylinux_wheel_abi3_file_match = re.compile( - f'^.*cp3.-abi3-manylinux({"|".join(manylinux_suffixes)})_x86_64.whl$' + f'^.*cp3.-abi3-manylinux({"|".join(manylinux_suffixes)})_{self.architecture}.whl$' + ) self.endpoint_urls = endpoint_urls @@ -600,7 +605,7 @@ def create_lambda_zip( # Make sure that 'concurrent' is always forbidden. # https://github.com/Miserlou/Zappa/issues/827 - if not "concurrent" in exclude: + if "concurrent" not in exclude: exclude.append("concurrent") def splitpath(path): @@ -1177,6 +1182,7 @@ def create_lambda_function( layers=None, concurrency=None, docker_image_uri=None, + architecture=None ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, a function name and a handler, register that Lambda function. @@ -1193,6 +1199,8 @@ def create_lambda_function( aws_kms_key_arn = "" if not layers: layers = [] + if not architecture: + self.architecture = "x86_64" kwargs = dict( FunctionName=function_name, @@ -1207,6 +1215,7 @@ def create_lambda_function( KMSKeyArn=aws_kms_key_arn, TracingConfig={"Mode": "Active" if self.xray_tracing else "PassThrough"}, Layers=layers, + Architectures=architecture, ) if not docker_image_uri: kwargs["Runtime"] = runtime @@ -1265,6 +1274,7 @@ def update_lambda_function( num_revisions=None, concurrency=None, docker_image_uri=None, + architecture=None ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, a function name and a handler, update that Lambda function's code. @@ -1358,6 +1368,7 @@ def update_lambda_configuration( aws_kms_key_arn=None, layers=None, wait=True, + architecture=None ): """ Given an existing function ARN, update the configuration variables. From 23c90f148d2717a694239faeac6be77848fd6f77 Mon Sep 17 00:00:00 2001 From: DS992 <> Date: Thu, 14 Apr 2022 22:08:25 +0200 Subject: [PATCH 005/111] Changed parameter in README to architecture --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f2fd3f34d..ef35205b0 100644 --- a/README.md +++ b/README.md @@ -833,7 +833,7 @@ to change Zappa's behavior. Use these at your own risk! "assume_policy": "my_assume_policy.json", // optional, IAM assume policy JSON file "attach_policy": "my_attach_policy.json", // optional, IAM attach policy JSON file "apigateway_policy": "my_apigateway_policy.json", // optional, API Gateway resource policy JSON file - "architectures": "x86_64", // optional, Set Lambda Architecture, defaults to x86_64. For Graviton 2 use: arm64 + "architecture": "x86_64", // optional, Set Lambda Architecture, defaults to x86_64. For Graviton 2 use: arm64 "async_source": "sns", // Source of async tasks. Defaults to "lambda" "async_resources": true, // Create the SNS topic and DynamoDB table to use. Defaults to true. "async_response_table": "your_dynamodb_table_name", // the DynamoDB table name to use for captured async responses; defaults to None (can't capture) From 5653c76d3e67b51b22f05bc237df1f5e50d83397 Mon Sep 17 00:00:00 2001 From: Bartosz Gwizdala Date: Tue, 19 Apr 2022 08:42:32 +0200 Subject: [PATCH 006/111] Support AWS Lambdas ephemeral storage setting (#1120) --- README.md | 3 ++- zappa/cli.py | 6 ++++++ zappa/core.py | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae4bcae0f..909c1229e 100644 --- a/README.md +++ b/README.md @@ -919,6 +919,7 @@ to change Zappa's behavior. Use these at your own risk! "log_level": "DEBUG", // Set the Zappa log level. Can be one of CRITICAL, ERROR, WARNING, INFO and DEBUG. Default: DEBUG "manage_roles": true, // Have Zappa automatically create and define IAM execution roles and policies. Default true. If false, you must define your own IAM Role and role_name setting. "memory_size": 512, // Lambda function memory in MB. Default 512. + "ephemeral_storage": { "Size": 512 }, // Lambda ephemeral storage memory in MB. Default 512. Max 10240. "num_retained_versions":null, // Indicates the number of old versions to retain for the lambda. If absent, keeps all the versions of the function. "payload_compression": true, // Whether or not to enable API gateway payload compression (default: true) "payload_minimum_compression_size": 0, // The threshold size (in bytes) below which payload compression will not be applied (default: 0) @@ -1005,7 +1006,7 @@ You can also simply handle CORS directly in your application. Your web framework ### Large Projects -AWS currently limits Lambda zip sizes to 50 megabytes. If your project is larger than that, set `slim_handler: true` in your `zappa_settings.json`. In this case, your fat application package will be replaced with a small handler-only package. The handler file then pulls the rest of the large project down from S3 at run time! The initial load of the large project may add to startup overhead, but the difference should be minimal on a warm lambda function. Note that this will also eat into the storage space of your application function. Note that AWS currently [limits](https://docs.aws.amazon.com/lambda/latest/dg/limits.html) the `/tmp` directory storage to 512 MB, so your project must still be smaller than that. +AWS currently limits Lambda zip sizes to 50 megabytes. If your project is larger than that, set `slim_handler: true` in your `zappa_settings.json`. In this case, your fat application package will be replaced with a small handler-only package. The handler file then pulls the rest of the large project down from S3 at run time! The initial load of the large project may add to startup overhead, but the difference should be minimal on a warm lambda function. Note that this will also eat into the storage space of your application function. Note that AWS [supports](https://aws.amazon.com/blogs/compute/using-larger-ephemeral-storage-for-aws-lambda/) custom `/tmp` directory storage size in a range of 512 - 10240 MB. Use `ephemeral_storage` in `zappa_settings.json` to adjust to your needs if your project is larger than default 512 MB. ### Enabling Bash Completion diff --git a/zappa/cli.py b/zappa/cli.py index 546e4b19c..59909d266 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -114,6 +114,7 @@ class ZappaCLI: handler_path = None vpc_config = None memory_size = None + ephemeral_storage = None use_apigateway = None lambda_handler = None django_settings = None @@ -905,6 +906,7 @@ def deploy(self, source_zip=None, docker_image_uri=None): dead_letter_config=self.dead_letter_config, timeout=self.timeout_seconds, memory_size=self.memory_size, + ephemeral_storage=self.ephemeral_storage, runtime=self.runtime, aws_environment_variables=self.aws_environment_variables, aws_kms_key_arn=self.aws_kms_key_arn, @@ -1167,6 +1169,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): vpc_config=self.vpc_config, timeout=self.timeout_seconds, memory_size=self.memory_size, + ephemeral_storage=self.ephemeral_storage, runtime=self.runtime, aws_environment_variables=self.aws_environment_variables, aws_kms_key_arn=self.aws_kms_key_arn, @@ -2526,6 +2529,9 @@ def load_settings(self, settings_file=None, session=None): ) self.vpc_config = self.stage_config.get("vpc_config", {}) self.memory_size = self.stage_config.get("memory_size", 512) + self.ephemeral_storage = self.stage_config.get( + "ephemeral_storage", {"Size": 512} + ) self.app_function = self.stage_config.get("app_function", None) self.exception_handler = self.stage_config.get("exception_handler", None) self.aws_region = self.stage_config.get("aws_region", None) diff --git a/zappa/core.py b/zappa/core.py index 51fa74836..c80de99bd 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1165,6 +1165,7 @@ def create_lambda_function( description="Zappa Deployment", timeout=30, memory_size=512, + ephemeral_storage={"Size": 512}, publish=True, vpc_config=None, dead_letter_config=None, @@ -1200,6 +1201,7 @@ def create_lambda_function( Description=description, Timeout=timeout, MemorySize=memory_size, + EphemeralStorage=ephemeral_storage, Publish=publish, VpcConfig=vpc_config, DeadLetterConfig=dead_letter_config, @@ -1351,6 +1353,7 @@ def update_lambda_configuration( description="Zappa Deployment", timeout=30, memory_size=512, + ephemeral_storage={"Size": 512}, publish=True, vpc_config=None, runtime="python3.6", @@ -1399,6 +1402,7 @@ def update_lambda_configuration( "Description": description, "Timeout": timeout, "MemorySize": memory_size, + "EphemeralStorage": ephemeral_storage, "VpcConfig": vpc_config, "Environment": {"Variables": aws_environment_variables}, "KMSKeyArn": aws_kms_key_arn, From 40579804abfc60f15675d3b4386c94927b1e159b Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 14 Jun 2022 15:02:03 +0800 Subject: [PATCH 007/111] Update __init__.py version bump --- zappa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 371973651..630c8be57 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.1" +__version__ = "0.54.2" From 758e00459c3adfa9a1c40158b8f91e0e93d22060 Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 14 Jun 2022 15:48:54 +0800 Subject: [PATCH 008/111] allow updating architectures for existing functions --- zappa/__init__.py | 2 +- zappa/core.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 630c8be57..02aea90bf 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.2" +__version__ = "0.54.3" diff --git a/zappa/core.py b/zappa/core.py index 16db6d770..6277fb71f 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -304,6 +304,9 @@ def __init__( if desired_role_arn: self.credentials_arn = desired_role_arn + if architecture: + self.architecture = architecture + self.runtime = runtime if self.runtime == "python3.6": @@ -1281,8 +1284,6 @@ def update_lambda_function( Optionally, delete previous versions if they exceed the optional limit. """ print("Updating Lambda function code..") - - kwargs = dict(FunctionName=function_name, Publish=publish) if docker_image_uri: kwargs["ImageUri"] = docker_image_uri elif local_zip: @@ -1374,7 +1375,6 @@ def update_lambda_configuration( Given an existing function ARN, update the configuration variables. """ print("Updating Lambda function configuration..") - if not vpc_config: vpc_config = {} if not self.credentials_arn: @@ -1414,6 +1414,7 @@ def update_lambda_configuration( "Environment": {"Variables": aws_environment_variables}, "KMSKeyArn": aws_kms_key_arn, "TracingConfig": {"Mode": "Active" if self.xray_tracing else "PassThrough"}, + "Architectures": architecture } if lambda_aws_config["PackageType"] != "Image": From a4428ed9fd8a12eb008f958fa59c7cee003eadab Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 14 Jun 2022 16:44:40 +0800 Subject: [PATCH 009/111] Update core.py --- zappa/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zappa/core.py b/zappa/core.py index 6277fb71f..8c899ddbf 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1284,6 +1284,7 @@ def update_lambda_function( Optionally, delete previous versions if they exceed the optional limit. """ print("Updating Lambda function code..") + kwargs = dict(FunctionName=function_name, Publish=publish, Architectures=architecture) if docker_image_uri: kwargs["ImageUri"] = docker_image_uri elif local_zip: From 097fa91a8dc695250ebd680b778457e88090f416 Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 14 Jun 2022 16:55:14 +0800 Subject: [PATCH 010/111] Update __init__.py --- zappa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 02aea90bf..8983cf350 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.3" +__version__ = "0.54.4" From 9c357d9cabb9a5d2cc3a91efa045cb31acb7b944 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 15 Jun 2022 10:11:15 +0800 Subject: [PATCH 011/111] regress bug fix --- zappa/__init__.py | 2 +- zappa/core.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 8983cf350..e0d833795 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.4" +__version__ = "0.54.5" diff --git a/zappa/core.py b/zappa/core.py index 8c899ddbf..1d7be98f2 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1415,7 +1415,6 @@ def update_lambda_configuration( "Environment": {"Variables": aws_environment_variables}, "KMSKeyArn": aws_kms_key_arn, "TracingConfig": {"Mode": "Active" if self.xray_tracing else "PassThrough"}, - "Architectures": architecture } if lambda_aws_config["PackageType"] != "Image": From 218cec9ef0a06245a7ee9dcf48179a1a6a928819 Mon Sep 17 00:00:00 2001 From: Sha Date: Fri, 17 Jun 2022 14:23:05 +0800 Subject: [PATCH 012/111] only delete zappa managed policies --- zappa/__init__.py | 2 +- zappa/core.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index e0d833795..18831e6a1 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.5" +__version__ = "0.54.6" diff --git a/zappa/core.py b/zappa/core.py index a8555f66a..e7d675c3d 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2935,15 +2935,17 @@ def _clear_policy(self, lambda_name): if policy_response["ResponseMetadata"]["HTTPStatusCode"] == 200: statement = json.loads(policy_response["Policy"])["Statement"] for s in statement: - delete_response = self.lambda_client.remove_permission( - FunctionName=lambda_name, StatementId=s["Sid"] - ) - if delete_response["ResponseMetadata"]["HTTPStatusCode"] != 204: - logger.error( - "Failed to delete an obsolete policy statement: {}".format( - policy_response - ) + if s['Sid'].startswith('zappa-'): + logger.debug(f"delete policy {s['Sid']}-{s['Principal']}") + delete_response = self.lambda_client.remove_permission( + FunctionName=lambda_name, StatementId=s["Sid"] ) + if delete_response["ResponseMetadata"]["HTTPStatusCode"] != 204: + logger.error( + "Failed to delete an obsolete policy statement: {}".format( + policy_response + ) + ) else: logger.debug( "Failed to load Lambda function policy: {}".format(policy_response) @@ -2971,7 +2973,7 @@ def create_event_permission(self, lambda_name, principal, source_arn): permission_response = self.lambda_client.add_permission( FunctionName=lambda_name, - StatementId="".join( + StatementId="zappa-"+"".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(8) ), Action="lambda:InvokeFunction", From 535afa384c242b8e42990bde95b33c7d437310b7 Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 20 Jun 2022 12:55:26 +0800 Subject: [PATCH 013/111] fix urlencode error, elb doesn't automatically decode url --- zappa/cli.py | 3 +-- zappa/core.py | 18 ++++++++++-------- zappa/wsgi.py | 21 +++++++++++++++++---- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index 09b5f582d..b69fd9cf7 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1179,7 +1179,6 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): layers=self.layers, wait=False, architecture=self.architecture, - ) # Finally, delete the local copy our zip package @@ -2641,7 +2640,7 @@ def load_settings(self, settings_file=None, session=None): tags=self.tags, endpoint_urls=self.stage_config.get("aws_endpoint_urls", {}), xray_tracing=self.xray_tracing, - architecture=self.architecture + architecture=self.architecture, ) for setting in CUSTOM_SETTINGS: diff --git a/zappa/core.py b/zappa/core.py index e7d675c3d..281231a35 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -284,7 +284,7 @@ def __init__( tags=(), endpoint_urls={}, xray_tracing=False, - architecture=None + architecture=None, ): """ Instantiate this new Zappa instance, loading any custom credentials if necessary. @@ -330,7 +330,6 @@ def __init__( ) self.manylinux_wheel_abi3_file_match = re.compile( f'^.*cp3.-abi3-manylinux({"|".join(manylinux_suffixes)})_{self.architecture}.whl$' - ) self.endpoint_urls = endpoint_urls @@ -1186,7 +1185,7 @@ def create_lambda_function( layers=None, concurrency=None, docker_image_uri=None, - architecture=None + architecture=None, ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, a function name and a handler, register that Lambda function. @@ -1279,14 +1278,16 @@ def update_lambda_function( num_revisions=None, concurrency=None, docker_image_uri=None, - architecture=None + architecture=None, ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, a function name and a handler, update that Lambda function's code. Optionally, delete previous versions if they exceed the optional limit. """ print("Updating Lambda function code..") - kwargs = dict(FunctionName=function_name, Publish=publish, Architectures=architecture) + kwargs = dict( + FunctionName=function_name, Publish=publish, Architectures=architecture + ) if docker_image_uri: kwargs["ImageUri"] = docker_image_uri elif local_zip: @@ -1373,7 +1374,7 @@ def update_lambda_configuration( aws_kms_key_arn=None, layers=None, wait=True, - architecture=None + architecture=None, ): """ Given an existing function ARN, update the configuration variables. @@ -2935,7 +2936,7 @@ def _clear_policy(self, lambda_name): if policy_response["ResponseMetadata"]["HTTPStatusCode"] == 200: statement = json.loads(policy_response["Policy"])["Statement"] for s in statement: - if s['Sid'].startswith('zappa-'): + if s["Sid"].startswith("zappa-"): logger.debug(f"delete policy {s['Sid']}-{s['Principal']}") delete_response = self.lambda_client.remove_permission( FunctionName=lambda_name, StatementId=s["Sid"] @@ -2973,7 +2974,8 @@ def create_event_permission(self, lambda_name, principal, source_arn): permission_response = self.lambda_client.add_permission( FunctionName=lambda_name, - StatementId="zappa-"+"".join( + StatementId="zappa-" + + "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(8) ), Action="lambda:InvokeFunction", diff --git a/zappa/wsgi.py b/zappa/wsgi.py index 63880e3fa..f8ce91cb4 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -41,12 +41,25 @@ def create_wsgi_request( we have to check for the existence of one and then fall back to the other. """ + + query_dict = {} + do_seq = False + query_string = "" if "multiValueQueryStringParameters" in event_info: - query = event_info["multiValueQueryStringParameters"] - query_string = urlencode(query, doseq=True) if query else "" + query_dict = event_info["multiValueQueryStringParameters"] + do_seq = True else: - query = event_info.get("queryStringParameters", {}) - query_string = urlencode(query) if query else "" + query_dict = event_info.get("queryStringParameters", {}) + + if query_dict: + # test query already encoded + # { + # 'where%3D%7B%22template%22%3A%20%2251f63e0838345b6dcd7eabff%22%7D': '' + # } + if len(query_dict) == 1 and list(query_dict.values())[0]: + query_string = list(query_dict.keys())[0] + else: + query_string = urlencode(query_dict, do_seq=do_seq) if context_header_mappings: for key, value in context_header_mappings.items(): From 7dbc2c1a7cc3545c071e2f5348af40d20f907add Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 20 Jun 2022 13:00:08 +0800 Subject: [PATCH 014/111] Update __init__.py --- zappa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 18831e6a1..ade092142 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.6" +__version__ = "0.54.7" From abc3096737fcbe739497c138dcf10b7b15f20173 Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 20 Jun 2022 16:47:25 +0800 Subject: [PATCH 015/111] urldecode bug fixes --- zappa/__init__.py | 2 +- zappa/wsgi.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index ade092142..24bb2bd3c 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.7" +__version__ = "0.54.8" diff --git a/zappa/wsgi.py b/zappa/wsgi.py index f8ce91cb4..bf2591822 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -59,7 +59,7 @@ def create_wsgi_request( if len(query_dict) == 1 and list(query_dict.values())[0]: query_string = list(query_dict.keys())[0] else: - query_string = urlencode(query_dict, do_seq=do_seq) + query_string = urlencode(query_dict, doseq=do_seq) if context_header_mappings: for key, value in context_header_mappings.items(): From 421484ed5318a99be31d4f4a7290de06360c38d2 Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 20 Jun 2022 17:04:26 +0800 Subject: [PATCH 016/111] fix query parameter buug --- zappa/__init__.py | 2 +- zappa/wsgi.py | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 24bb2bd3c..5756d8932 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.8" +__version__ = "0.54.9" diff --git a/zappa/wsgi.py b/zappa/wsgi.py index bf2591822..154f62269 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -1,7 +1,7 @@ import base64 import logging import sys -from urllib.parse import urlencode +from urllib.parse import urlencode, unquote_plus import six from requestlogger import ApacheFormatter @@ -45,21 +45,30 @@ def create_wsgi_request( query_dict = {} do_seq = False query_string = "" - if "multiValueQueryStringParameters" in event_info: + if "queryStringParameters" in event_info: + query_dict = event_info.get("queryStringParameters", {}) + else: query_dict = event_info["multiValueQueryStringParameters"] do_seq = True - else: - query_dict = event_info.get("queryStringParameters", {}) - if query_dict: + new_query_dict = {} + for ( + key, + value, + ) in query_dict.items(): # test query already encoded # { # 'where%3D%7B%22template%22%3A%20%2251f63e0838345b6dcd7eabff%22%7D': '' # } - if len(query_dict) == 1 and list(query_dict.values())[0]: - query_string = list(query_dict.keys())[0] - else: - query_string = urlencode(query_dict, doseq=do_seq) + # { + # 'where={"template": "51f63e0838345b6dcd7eabff"}': '' + # } + new_query_dict[unquote_plus(key)] = ( + value if isinstance(value, list) else unquote_plus(value) + ) + + if new_query_dict: + query_string = urlencode(new_query_dict, doseq=do_seq) if context_header_mappings: for key, value in context_header_mappings.items(): From 26295ce13498fa6737feb018fa031e92f275b0a0 Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 20 Jun 2022 17:08:58 +0800 Subject: [PATCH 017/111] Update wsgi.py --- zappa/wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/wsgi.py b/zappa/wsgi.py index 154f62269..a9872ed14 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -48,7 +48,7 @@ def create_wsgi_request( if "queryStringParameters" in event_info: query_dict = event_info.get("queryStringParameters", {}) else: - query_dict = event_info["multiValueQueryStringParameters"] + query_dict = event_info.get("multiValueQueryStringParameters", {}) do_seq = True new_query_dict = {} From 50d90af9a8da1e21484f288c7647f6b98b14768c Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 20 Jun 2022 17:32:01 +0800 Subject: [PATCH 018/111] fix query parameter bug --- zappa/__init__.py | 2 +- zappa/wsgi.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 5756d8932..24a6db4b4 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.9" +__version__ = "0.54.10" diff --git a/zappa/wsgi.py b/zappa/wsgi.py index a9872ed14..2739ec6be 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -45,9 +45,10 @@ def create_wsgi_request( query_dict = {} do_seq = False query_string = "" - if "queryStringParameters" in event_info: + print(event_info) + if event_info.get("queryStringParameters"): query_dict = event_info.get("queryStringParameters", {}) - else: + elif event_info.get("multiValueQueryStringParameters"): query_dict = event_info.get("multiValueQueryStringParameters", {}) do_seq = True From 7caac2498204f2c70af9f92cdf7b0be6f7b4301c Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 21 Jun 2022 09:22:40 +0800 Subject: [PATCH 019/111] Update __init__.py version bump --- zappa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 24a6db4b4..6e7fdcd10 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -13,4 +13,4 @@ ) raise RuntimeError(err_msg) -__version__ = "0.54.10" +__version__ = "0.54.11" From f1881b645021493b52f1cdd9420760b1b9baa211 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 10:17:19 +0900 Subject: [PATCH 020/111] :wrench: migrate https://github.com/zappa/Zappa/pull/971 to lastest master --- tests/test_binary_support_settings.py | 12 +++ tests/test_handler.py | 128 +++++++++++++++++++++++++- tests/test_wsgi_binary_support_app.py | 63 +++++++++++++ tests/utils.py | 14 ++- zappa/handler.py | 26 +++++- 5 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 tests/test_binary_support_settings.py create mode 100644 tests/test_wsgi_binary_support_app.py diff --git a/tests/test_binary_support_settings.py b/tests/test_binary_support_settings.py new file mode 100644 index 000000000..484fc74dd --- /dev/null +++ b/tests/test_binary_support_settings.py @@ -0,0 +1,12 @@ +API_STAGE = "dev" +APP_FUNCTION = "app" +APP_MODULE = "tests.test_wsgi_binary_support_app" +BINARY_SUPPORT = True +CONTEXT_HEADER_MAPPINGS = {} +DEBUG = "True" +DJANGO_SETTINGS = None +DOMAIN = "api.example.com" +ENVIRONMENT_VARIABLES = {} +LOG_LEVEL = "DEBUG" +PROJECT_NAME = "binary_support_settings" +COGNITO_TRIGGER_MAPPING = {} \ No newline at end of file diff --git a/tests/test_handler.py b/tests/test_handler.py index cc0590128..6beea38fd 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,11 +1,11 @@ -import sys import unittest - from mock import Mock from zappa.handler import LambdaHandler from zappa.utilities import merge_headers +from .utils import is_base64 + def no_args(): return @@ -223,6 +223,130 @@ def test_exception_handler_on_web_request(self): self.assertEqual(response["statusCode"], 500) mocked_exception_handler.assert_called() + def test_wsgi_script_binary_support_with_content_encoding(self): + """ + Ensure that response body is base64 encoded when BINARY_SUPPORT is enabled and Content-Encoding header is present. + """ # don't linebreak so that whole line is shown during nosetest readout + lh = LambdaHandler("tests.test_binary_support_settings") + + text_plain_event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/content_encoding_header_json1", + } + + # A likely scenario is that the application would be gzip compressing some json response. That's checked first. + response = lh.handler(text_plain_event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + + # We also verify that some unknown mimetype with a Content-Encoding also encodes to b64. This route serves + # bytes in the response. + + text_arbitrary_event = { + **text_plain_event, + **{"path": "/content_encoding_header_textarbitrary1"}, + } + + response = lh.handler(text_arbitrary_event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + + # This route is similar to the above, but it serves its response as text and not bytes. That the response + # isn't bytes shouldn't matter because it still has a Content-Encoding header. + + application_json_event = { + **text_plain_event, + **{"path": "/content_encoding_header_textarbitrary2"}, + } + + response = lh.handler(application_json_event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + + def test_wsgi_script_binary_support_without_content_encoding_edgecases( + self, + ): + """ + Ensure zappa response bodies are NOT base64 encoded when BINARY_SUPPORT is enabled and the mimetype is "application/json" or starts with "text/". + """ # don't linebreak so that whole line is shown during nosetest readout + + lh = LambdaHandler("tests.test_binary_support_settings") + + text_plain_event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/textplain_mimetype_response1", + } + + for path in [ + "/textplain_mimetype_response1", # text/plain mimetype should not be turned to base64 + "/textarbitrary_mimetype_response1", # text/arbitrary mimetype should not be turned to base64 + "/json_mimetype_response1", # application/json mimetype should not be turned to base64 + ]: + event = {**text_plain_event, "path": path} + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertNotIn("isBase64Encoded", response) + self.assertFalse(is_base64(response["body"])) + + def test_wsgi_script_binary_support_without_content_encoding( + self, + ): + """ + Ensure zappa response bodies are base64 encoded when BINARY_SUPPORT is enabled and Content-Encoding is absent. + """ # don't linebreak so that whole line is shown during nosetest readout + + lh = LambdaHandler("tests.test_binary_support_settings") + + text_plain_event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/textplain_mimetype_response1", + } + + for path in [ + "/arbitrarybinary_mimetype_response1", + "/arbitrarybinary_mimetype_response2", + ]: + event = {**text_plain_event, "path": path} + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + def test_wsgi_script_on_cognito_event_request(self): """ Ensure that requests sent by cognito behave sensibly diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py new file mode 100644 index 000000000..0fbdd9a5f --- /dev/null +++ b/tests/test_wsgi_binary_support_app.py @@ -0,0 +1,63 @@ +### +# This test application exists to confirm how Zappa handles WSGI application +# _responses_ when Binary Support is enabled. +### + +import gzip +import json + +from flask import Flask, Response, send_file + +app = Flask(__name__) + + +@app.route("/textplain_mimetype_response1", methods=["GET"]) +def text_mimetype_response_1(): + return Response(response="OK", mimetype="text/plain") + + +@app.route("/textarbitrary_mimetype_response1", methods=["GET"]) +def text_mimetype_response_2(): + return Response(response="OK", mimetype="text/arbitary") + + +@app.route("/json_mimetype_response1", methods=["GET"]) +def json_mimetype_response_1(): + return Response(response=json.dumps({"some": "data"}), mimetype="application/json") + + +@app.route("/arbitrarybinary_mimetype_response1", methods=["GET"]) +def arbitrary_mimetype_response_1(): + return Response(response=b"some binary data", mimetype="arbitrary/binary_mimetype") + + +@app.route("/arbitrarybinary_mimetype_response2", methods=["GET"]) +def arbitrary_mimetype_response_3(): + return Response(response="doesnt_matter", mimetype="definitely_not_text") + + +@app.route("/content_encoding_header_json1", methods=["GET"]) +def response_with_content_encoding_1(): + return Response( + response=gzip.compress(json.dumps({"some": "data"}).encode()), + mimetype="application/json", + headers={"Content-Encoding": "gzip"}, + ) + + +@app.route("/content_encoding_header_textarbitrary1", methods=["GET"]) +def response_with_content_encoding_2(): + return Response( + response=b"OK", + mimetype="text/arbitrary", + headers={"Content-Encoding": "something_arbitrarily_binary"}, + ) + + +@app.route("/content_encoding_header_textarbitrary2", methods=["GET"]) +def response_with_content_encoding_3(): + return Response( + response="OK", + mimetype="text/arbitrary", + headers={"Content-Encoding": "with_content_type_but_not_bytes_response"}, + ) \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py index 91e588cc4..8a2a93fad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,4 @@ +import base64 import functools import os from contextlib import contextmanager @@ -6,10 +7,7 @@ import placebo from mock import MagicMock, patch -try: - file -except NameError: # builtin 'file' was removed in Python 3 - from io import IOBase as file +from io import IOBase as file PLACEBO_DIR = os.path.join(os.path.dirname(__file__), "placebo") @@ -72,3 +70,11 @@ def stub_open(*args, **kwargs): with patch("__builtin__.open", stub_open): yield mock_open, mock_file + + +def is_base64(test_string: str) -> bool: + # Taken from https://stackoverflow.com/a/45928164/3200002 + try: + return base64.b64encode(base64.b64decode(test_string)).decode() == test_string + except Exception: + return False \ No newline at end of file diff --git a/zappa/handler.py b/zappa/handler.py index ed0cc9835..f44274c3b 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -557,12 +557,28 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - if ( - settings.BINARY_SUPPORT - and not response.mimetype.startswith("text/") - and response.mimetype != "application/json" + # We base64 encode for two reasons when BINARY_SUPPORT is enabled: + # - Content-Encoding is present, which is commonly used by compression mechanisms to indicate + # that the content is in br/gzip/deflate/etc encoding + # (Related: https://github.com/zappa/Zappa/issues/908). Content like this must be + # transmitted as b64. + # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it + # isn't application/json or text/) + if settings.BINARY_SUPPORT and response.headers.get( + "Content-Encoding" ): - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf-8") + zappa_returndict["body"] = base64.b64encode( + response.data + ).decode() + zappa_returndict["isBase64Encoded"] = True + elif ( + settings.BINARY_SUPPORT + and not response.mimetype.startswith("text/") + and response.mimetype != "application/json" + ): + zappa_returndict["body"] = base64.b64encode( + response.data + ).decode("utf8") zappa_returndict["isBase64Encoded"] = True else: zappa_returndict["body"] = response.get_data(as_text=True) From 19f74a9b7ce3af6aff6897af46ae299437b6bdd7 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 10:22:19 +0900 Subject: [PATCH 021/111] :art: run black/isort --- tests/test_binary_support_settings.py | 2 +- tests/test_handler.py | 1 + tests/test_wsgi_binary_support_app.py | 2 +- tests/utils.py | 5 ++--- zappa/handler.py | 18 ++++++------------ 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/test_binary_support_settings.py b/tests/test_binary_support_settings.py index 484fc74dd..adb257d55 100644 --- a/tests/test_binary_support_settings.py +++ b/tests/test_binary_support_settings.py @@ -9,4 +9,4 @@ ENVIRONMENT_VARIABLES = {} LOG_LEVEL = "DEBUG" PROJECT_NAME = "binary_support_settings" -COGNITO_TRIGGER_MAPPING = {} \ No newline at end of file +COGNITO_TRIGGER_MAPPING = {} diff --git a/tests/test_handler.py b/tests/test_handler.py index 6beea38fd..5cf02c5b9 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,4 +1,5 @@ import unittest + from mock import Mock from zappa.handler import LambdaHandler diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py index 0fbdd9a5f..d1d2e6638 100644 --- a/tests/test_wsgi_binary_support_app.py +++ b/tests/test_wsgi_binary_support_app.py @@ -60,4 +60,4 @@ def response_with_content_encoding_3(): response="OK", mimetype="text/arbitrary", headers={"Content-Encoding": "with_content_type_but_not_bytes_response"}, - ) \ No newline at end of file + ) diff --git a/tests/utils.py b/tests/utils.py index 8a2a93fad..c779adf67 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,13 +2,12 @@ import functools import os from contextlib import contextmanager +from io import IOBase as file import boto3 import placebo from mock import MagicMock, patch -from io import IOBase as file - PLACEBO_DIR = os.path.join(os.path.dirname(__file__), "placebo") @@ -77,4 +76,4 @@ def is_base64(test_string: str) -> bool: try: return base64.b64encode(base64.b64decode(test_string)).decode() == test_string except Exception: - return False \ No newline at end of file + return False diff --git a/zappa/handler.py b/zappa/handler.py index f44274c3b..c19fee9d4 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -564,21 +564,15 @@ def handler(self, event, context): # transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it # isn't application/json or text/) - if settings.BINARY_SUPPORT and response.headers.get( - "Content-Encoding" - ): - zappa_returndict["body"] = base64.b64encode( - response.data - ).decode() + if settings.BINARY_SUPPORT and response.headers.get("Content-Encoding"): + zappa_returndict["body"] = base64.b64encode(response.data).decode() zappa_returndict["isBase64Encoded"] = True elif ( - settings.BINARY_SUPPORT - and not response.mimetype.startswith("text/") - and response.mimetype != "application/json" + settings.BINARY_SUPPORT + and not response.mimetype.startswith("text/") + and response.mimetype != "application/json" ): - zappa_returndict["body"] = base64.b64encode( - response.data - ).decode("utf8") + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") zappa_returndict["isBase64Encoded"] = True else: zappa_returndict["body"] = response.get_data(as_text=True) From d7fcee4cd1994a522e50bc157d21325ae28df645 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 13:48:33 +0900 Subject: [PATCH 022/111] :recycle: refactor to allow for other binary ignore types based on mimetype. (currently openapi schema can't be passed as text. --- zappa/handler.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index c19fee9d4..1e8359387 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -560,22 +560,19 @@ def handler(self, event, context): # We base64 encode for two reasons when BINARY_SUPPORT is enabled: # - Content-Encoding is present, which is commonly used by compression mechanisms to indicate # that the content is in br/gzip/deflate/etc encoding - # (Related: https://github.com/zappa/Zappa/issues/908). Content like this must be - # transmitted as b64. - # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it - # isn't application/json or text/) - if settings.BINARY_SUPPORT and response.headers.get("Content-Encoding"): - zappa_returndict["body"] = base64.b64encode(response.data).decode() - zappa_returndict["isBase64Encoded"] = True - elif ( - settings.BINARY_SUPPORT - and not response.mimetype.startswith("text/") - and response.mimetype != "application/json" - ): - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True - else: - zappa_returndict["body"] = response.get_data(as_text=True) + # (Related: https://github.com/zappa/Zappa/issues/908). + # Content like this must be transmitted as b64. + # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) + zappa_returndict["body"] = response.get_data(as_text=True) + if settings.BINARY_SUPPORT: + # overwrite zappa_returndict["body"] if necessary + exclude_startswith_mimetypes = ("text/", "application/json", "application/vnd.oai.openapi") # TODO: consider for settings + if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") + zappa_returndict["isBase64Encoded"] = True + elif not response.mimetype.startswith(exclude_startswith_mimetypes): + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") + zappa_returndict["isBase64Encoded"] = True zappa_returndict["statusCode"] = response.status_code if "headers" in event: From 039cbfedf10e330ae7bca09827ff2efeb764d566 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 13:53:16 +0900 Subject: [PATCH 023/111] :art: run black/fix flake8 --- zappa/handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zappa/handler.py b/zappa/handler.py index 1e8359387..0345e12fc 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -566,7 +566,11 @@ def handler(self, event, context): zappa_returndict["body"] = response.get_data(as_text=True) if settings.BINARY_SUPPORT: # overwrite zappa_returndict["body"] if necessary - exclude_startswith_mimetypes = ("text/", "application/json", "application/vnd.oai.openapi") # TODO: consider for settings + exclude_startswith_mimetypes = ( + "text/", + "application/json", + "application/vnd.oai.openapi", + ) # TODO: consider for settings if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") zappa_returndict["isBase64Encoded"] = True From bcdfbe4020674cf8c97b4ce01058253c2802c752 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 13:59:37 +0900 Subject: [PATCH 024/111] :wrench: add EXCEPTION_HANDLER setting --- tests/test_binary_support_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_binary_support_settings.py b/tests/test_binary_support_settings.py index adb257d55..84713e889 100644 --- a/tests/test_binary_support_settings.py +++ b/tests/test_binary_support_settings.py @@ -10,3 +10,4 @@ LOG_LEVEL = "DEBUG" PROJECT_NAME = "binary_support_settings" COGNITO_TRIGGER_MAPPING = {} +EXCEPTION_HANDLER = None From 3f0d1357c9c32672251af4349caa8f841b26f193 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 14:05:50 +0900 Subject: [PATCH 025/111] :bug: fix zappa_returndict["body"] assignment --- zappa/handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 0345e12fc..55db6cf70 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -563,9 +563,7 @@ def handler(self, event, context): # (Related: https://github.com/zappa/Zappa/issues/908). # Content like this must be transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) - zappa_returndict["body"] = response.get_data(as_text=True) if settings.BINARY_SUPPORT: - # overwrite zappa_returndict["body"] if necessary exclude_startswith_mimetypes = ( "text/", "application/json", @@ -578,6 +576,10 @@ def handler(self, event, context): zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") zappa_returndict["isBase64Encoded"] = True + if "body" not in zappa_returndict: + # treat body as text + zappa_returndict["body"] = response.get_data(as_text=True) + zappa_returndict["statusCode"] = response.status_code if "headers" in event: zappa_returndict["headers"] = {} From 583cc4d1c333c0b7a77af165df32702e89da7959 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 14:25:13 +0900 Subject: [PATCH 026/111] :pencil: add temp debug info --- zappa/handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zappa/handler.py b/zappa/handler.py index 55db6cf70..1793adc38 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -563,6 +563,9 @@ def handler(self, event, context): # (Related: https://github.com/zappa/Zappa/issues/908). # Content like this must be transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) + print(f"settings.BINARY_SUPPORT={settings.BINARY_SUPPORT}") + print(f"response.headers.get('Content-Encoding')={response.headers.get('Content-Encoding')}") + print(f"response.mimetype={response.mimetype}") if settings.BINARY_SUPPORT: exclude_startswith_mimetypes = ( "text/", From 20bd12fdb7e9ee74f337741966cccf2ea2701226 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 15:20:39 +0900 Subject: [PATCH 027/111] :fire: delete unnecessary print statements --- zappa/handler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 1793adc38..55db6cf70 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -563,9 +563,6 @@ def handler(self, event, context): # (Related: https://github.com/zappa/Zappa/issues/908). # Content like this must be transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) - print(f"settings.BINARY_SUPPORT={settings.BINARY_SUPPORT}") - print(f"response.headers.get('Content-Encoding')={response.headers.get('Content-Encoding')}") - print(f"response.mimetype={response.mimetype}") if settings.BINARY_SUPPORT: exclude_startswith_mimetypes = ( "text/", From 2a6aacd84092c4077d22deb48653904f6ab2150d Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 16:40:30 +0900 Subject: [PATCH 028/111] :recycle: Update comments and minor refactor for clarity --- zappa/handler.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 55db6cf70..9591a48c6 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -557,27 +557,36 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - # We base64 encode for two reasons when BINARY_SUPPORT is enabled: - # - Content-Encoding is present, which is commonly used by compression mechanisms to indicate - # that the content is in br/gzip/deflate/etc encoding - # (Related: https://github.com/zappa/Zappa/issues/908). - # Content like this must be transmitted as b64. - # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) + encode_as_base64 = False if settings.BINARY_SUPPORT: + # Related: https://github.com/zappa/Zappa/issues/908 + # API Gateway requires binary data be base64 encoded: + # https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis/ + # When BINARY_SUPPORT is enabled the body is base64 encoded in the following cases: + # - Content-Encoding defined, commonly used to specify compression (br/gzip/deflate/etc) + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + # Content like this must be transmitted as b64. + # - Response assumed binary when Response.mimetype does + # not start with entry defined in 'exclude_startswith_mimetypes' exclude_startswith_mimetypes = ( "text/", "application/json", "application/vnd.oai.openapi", ) # TODO: consider for settings if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True + encode_as_base64 = True + + # werkzeug Response.mimetype: lowercase without parameters + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.mimetype elif not response.mimetype.startswith(exclude_startswith_mimetypes): - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True + encode_as_base64 = True - if "body" not in zappa_returndict: - # treat body as text + if encode_as_base64: + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") + zappa_returndict["isBase64Encoded"] = True + else: + # response.data decoded by werkzeug + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.get_data zappa_returndict["body"] = response.get_data(as_text=True) zappa_returndict["statusCode"] = response.status_code From 153366d69c1ce6087c2835d3e819b3ae6dfa667c Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 29 Jul 2022 09:59:13 +0900 Subject: [PATCH 029/111] :recycle: refactor for ease of testing and clarity --- zappa/handler.py | 79 +++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 9591a48c6..3d952e00e 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -10,6 +10,7 @@ import tarfile import traceback from builtins import str +from typing import Tuple import boto3 from werkzeug.wrappers import Response @@ -265,6 +266,49 @@ def _process_exception(cls, exception_handler, event, context, exception): print(cex) return exception_processed + @staticmethod + def _process_response_body(response: Response, binary_support: bool = False) -> Tuple[str, bool]: + """ + Perform Response body encoding/decoding + + Related: https://github.com/zappa/Zappa/issues/908 + API Gateway requires binary data be base64 encoded: + https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis/ + When BINARY_SUPPORT is enabled the body is base64 encoded in the following cases: + + - Content-Encoding defined, commonly used to specify compression (br/gzip/deflate/etc) + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + Content like this must be transmitted as b64. + + - Response assumed binary when Response.mimetype does + not start with an entry defined in 'handle_as_text_mimetypes' + """ + encode_body_as_base64 = False + if binary_support: + + handle_as_text_mimetypes = ( + "text/", + "application/json", + "application/vnd.oai.openapi", + ) # TODO: consider for settings + # TODO: woff files ok? + if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding + encode_body_as_base64 = True + + # werkzeug Response.mimetype: lowercase without parameters + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.mimetype + elif not response.mimetype.startswith(handle_as_text_mimetypes): + encode_body_as_base64 = True + + if encode_body_as_base64: + body = base64.b64encode(response.data).decode("utf8") + else: + # response.data decoded by werkzeug + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.get_data + body = response.get_data(as_text=True) + + return body, encode_body_as_base64 + @staticmethod def run_function(app_function, event, context): """ @@ -557,37 +601,10 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - encode_as_base64 = False - if settings.BINARY_SUPPORT: - # Related: https://github.com/zappa/Zappa/issues/908 - # API Gateway requires binary data be base64 encoded: - # https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis/ - # When BINARY_SUPPORT is enabled the body is base64 encoded in the following cases: - # - Content-Encoding defined, commonly used to specify compression (br/gzip/deflate/etc) - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding - # Content like this must be transmitted as b64. - # - Response assumed binary when Response.mimetype does - # not start with entry defined in 'exclude_startswith_mimetypes' - exclude_startswith_mimetypes = ( - "text/", - "application/json", - "application/vnd.oai.openapi", - ) # TODO: consider for settings - if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding - encode_as_base64 = True - - # werkzeug Response.mimetype: lowercase without parameters - # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.mimetype - elif not response.mimetype.startswith(exclude_startswith_mimetypes): - encode_as_base64 = True - - if encode_as_base64: - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True - else: - # response.data decoded by werkzeug - # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.get_data - zappa_returndict["body"] = response.get_data(as_text=True) + processed_body, is_base64_encoded = self._process_response_body(response, binary_support=settings.BINARY_SUPPORT) + zappa_returndict["body"] = processed_body + if is_base64_encoded: + zappa_returndict["isBase64Encoded"] = is_base64_encoded zappa_returndict["statusCode"] = response.status_code if "headers" in event: From 4ddfaa53a8c179a4cbca597270eb9d57411cbdb9 Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 29 Jul 2022 10:21:13 +0900 Subject: [PATCH 030/111] :art: fix flake8 --- zappa/handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zappa/handler.py b/zappa/handler.py index 3d952e00e..a075d6046 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -601,7 +601,9 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - processed_body, is_base64_encoded = self._process_response_body(response, binary_support=settings.BINARY_SUPPORT) + processed_body, is_base64_encoded = self._process_response_body( + response, binary_support=settings.BINARY_SUPPORT + ) zappa_returndict["body"] = processed_body if is_base64_encoded: zappa_returndict["isBase64Encoded"] = is_base64_encoded From 0370119a794a3356f5158fb19135cb46a952fad5 Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 5 Aug 2022 18:44:18 +0900 Subject: [PATCH 031/111] :sparkles: add `additional_text_mimetypes` setting :white_check_mark: add testcases for additional_text_mimetypes handling --- README.md | 1 + test_settings.json | 9 ++- ...ad_additional_text_mimetypes_settings.json | 9 +++ ...port_additional_text_mimetypes_settings.py | 14 +++++ tests/test_handler.py | 58 +++++++++++++++++++ tests/test_wsgi_binary_support_app.py | 8 +++ tests/tests.py | 14 +++++ zappa/cli.py | 11 ++++ zappa/handler.py | 21 +++---- 9 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 tests/test_bad_additional_text_mimetypes_settings.json create mode 100644 tests/test_binary_support_additional_text_mimetypes_settings.py diff --git a/README.md b/README.md index 911860667..c56df1f51 100644 --- a/README.md +++ b/README.md @@ -820,6 +820,7 @@ to change Zappa's behavior. Use these at your own risk! ```javascript { "dev": { + "additional_text_mimetypes": [], // allows you to provide additional mimetypes to be handled as text when binary_support is true. "alb_enabled": false, // enable provisioning of application load balancing resources. If set to true, you _must_ fill out the alb_vpc_config option as well. "alb_vpc_config": { "CertificateArn": "your_acm_certificate_arn", // ACM certificate ARN for ALB diff --git a/test_settings.json b/test_settings.json index f7b29bfc2..2ee126a1f 100644 --- a/test_settings.json +++ b/test_settings.json @@ -120,5 +120,12 @@ "lambda_concurrency_enabled": { "extends": "ttt888", "lambda_concurrency": 6 - } + }, + "addtextmimetypes": { + "s3_bucket": "lmbda", + "app_function": "tests.test_app.hello_world", + "delete_local_zip": true, + "binary_support": true, + "additional_text_mimetypes": ["application/custommimetype"] + } } diff --git a/tests/test_bad_additional_text_mimetypes_settings.json b/tests/test_bad_additional_text_mimetypes_settings.json new file mode 100644 index 000000000..b3a09f57f --- /dev/null +++ b/tests/test_bad_additional_text_mimetypes_settings.json @@ -0,0 +1,9 @@ +{ + "nobinarysupport": { + "s3_bucket": "lmbda", + "app_function": "tests.test_app.hello_world", + "delete_local_zip": true, + "binary_support": false, + "additional_text_mimetypes": ["application/custommimetype"] + } +} \ No newline at end of file diff --git a/tests/test_binary_support_additional_text_mimetypes_settings.py b/tests/test_binary_support_additional_text_mimetypes_settings.py new file mode 100644 index 000000000..27c70c1bc --- /dev/null +++ b/tests/test_binary_support_additional_text_mimetypes_settings.py @@ -0,0 +1,14 @@ +API_STAGE = "dev" +APP_FUNCTION = "app" +APP_MODULE = "tests.test_wsgi_binary_support_app" +BINARY_SUPPORT = True +CONTEXT_HEADER_MAPPINGS = {} +DEBUG = "True" +DJANGO_SETTINGS = None +DOMAIN = "api.example.com" +ENVIRONMENT_VARIABLES = {} +LOG_LEVEL = "DEBUG" +PROJECT_NAME = "binary_support_settings" +COGNITO_TRIGGER_MAPPING = {} +EXCEPTION_HANDLER = None +ADDITIONAL_TEXT_MIMETYPES = ["application/vnd.oai.openapi"] diff --git a/tests/test_handler.py b/tests/test_handler.py index 5cf02c5b9..b8cb59fee 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -348,6 +348,64 @@ def test_wsgi_script_binary_support_without_content_encoding( self.assertIn("isBase64Encoded", response) self.assertTrue(is_base64(response["body"])) + def test_wsgi_script_binary_support_userdefined_additional_text_mimetypes__defined( + self, + ): + """ + Ensure zappa response bodies are NOT base64 encoded when BINARY_SUPPORT is True, and additional_text_mimetypes are defined + """ + lh = LambdaHandler("tests.test_binary_support_additional_text_mimetypes_settings") + expected_additional_mimetypes = ["application/vnd.oai.openapi"] + self.assertEqual(lh.settings.ADDITIONAL_TEXT_MIMETYPES, expected_additional_mimetypes) + + event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/userdefined_additional_mimetype_response1", + } + + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertNotIn("isBase64Encoded", response) + self.assertFalse(is_base64(response["body"])) + + def test_wsgi_script_binary_support_userdefined_additional_text_mimetypes__undefined( + self, + ): + """ + Ensure zappa response bodies are base64 encoded when BINARY_SUPPORT is True and mimetype not defined in additional_text_mimetypes + """ + lh = LambdaHandler("tests.test_binary_support_settings") + + event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/userdefined_additional_mimetype_response1", + } + + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + def test_wsgi_script_on_cognito_event_request(self): """ Ensure that requests sent by cognito behave sensibly diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py index d1d2e6638..b4c4ea504 100644 --- a/tests/test_wsgi_binary_support_app.py +++ b/tests/test_wsgi_binary_support_app.py @@ -61,3 +61,11 @@ def response_with_content_encoding_3(): mimetype="text/arbitrary", headers={"Content-Encoding": "with_content_type_but_not_bytes_response"}, ) + + +@app.route("/userdefined_additional_mimetype_response1", methods=["GET"]) +def response_with_userdefined_addtional_mimetype(): + return Response( + response="OK", + mimetype="application/vnd.oai.openapi", + ) diff --git a/tests/tests.py b/tests/tests.py index 831574606..61f572e5e 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1074,6 +1074,20 @@ def test_load_settings_toml(self): zappa_cli.load_settings("tests/test_settings.toml") self.assertEqual(False, zappa_cli.stage_config["touch"]) + def test_load_settings_bad_additional_text_mimetypes(self): + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "nobinarysupport" + with self.assertRaises(ClickException): + zappa_cli.load_settings("tests/test_bad_additional_text_mimetypes_settings.json") + + def test_load_settings_additional_text_mimetypes(self): + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "addtextmimetypes" + zappa_cli.load_settings("test_settings.json") + expected_additional_text_mimetypes = ["application/custommimetype"] + self.assertEqual(expected_additional_text_mimetypes, zappa_cli.stage_config["additional_text_mimetypes"]) + self.assertEqual(True, zappa_cli.stage_config["binary_support"]) + def test_settings_extension(self): """ Make sure Zappa uses settings in the proper order: JSON, TOML, YAML. diff --git a/zappa/cli.py b/zappa/cli.py index 305e74e30..0e4ac6272 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -67,6 +67,7 @@ BOTO3_CONFIG_DOCS_URL = "https://boto3.readthedocs.io/en/latest/guide/quickstart.html#configuration" + ## # Main Input Processing ## @@ -117,6 +118,7 @@ class ZappaCLI: xray_tracing = False aws_kms_key_arn = "" context_header_mappings = None + additional_text_mimetypes = None tags = [] layers = None @@ -2290,6 +2292,11 @@ def load_settings(self, settings_file=None, session=None): self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") self.layers = self.stage_config.get("layers", None) + self.additional_text_mimetypes = self.stage_config.get("additional_text_mimetypes", None) + + # check that BINARY_SUPPORT is True if additional_text_mimetypes is provided + if self.additional_text_mimetypes and not self.binary_support: + raise ClickException("zappa_settings.json has additional_text_mimetypes defined, but binary_support is False!") # Load ALB-related settings self.use_alb = self.stage_config.get("alb_enabled", False) @@ -2622,6 +2629,10 @@ def get_zappa_settings_string(self): # async response async_response_table = self.stage_config.get("async_response_table", "") settings_s += "ASYNC_RESPONSE_TABLE='{0!s}'\n".format(async_response_table) + + # additional_text_mimetypes + additional_text_mimetypes = self.stage_config.get("additional_text_mimetypes", []) + settings_s += f"ADDITIONAL_TEXT_MIMETYPES={additional_text_mimetypes}\n" return settings_s def remove_local_zip(self): diff --git a/zappa/handler.py b/zappa/handler.py index a075d6046..88976b4d7 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -10,6 +10,7 @@ import tarfile import traceback from builtins import str +from types import ModuleType from typing import Tuple import boto3 @@ -267,7 +268,7 @@ def _process_exception(cls, exception_handler, event, context, exception): return exception_processed @staticmethod - def _process_response_body(response: Response, binary_support: bool = False) -> Tuple[str, bool]: + def _process_response_body(response: Response, settings: ModuleType) -> Tuple[str, bool]: """ Perform Response body encoding/decoding @@ -284,14 +285,12 @@ def _process_response_body(response: Response, binary_support: bool = False) -> not start with an entry defined in 'handle_as_text_mimetypes' """ encode_body_as_base64 = False - if binary_support: - - handle_as_text_mimetypes = ( - "text/", - "application/json", - "application/vnd.oai.openapi", - ) # TODO: consider for settings - # TODO: woff files ok? + if settings.BINARY_SUPPORT: + handle_as_text_mimetypes = ("text/", "application/json") + additional_text_mimetypes = getattr(settings, "ADDITIONAL_TEXT_MIMETYPES", None) + if additional_text_mimetypes: + handle_as_text_mimetypes += tuple(additional_text_mimetypes) + if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding encode_body_as_base64 = True @@ -601,9 +600,7 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - processed_body, is_base64_encoded = self._process_response_body( - response, binary_support=settings.BINARY_SUPPORT - ) + processed_body, is_base64_encoded = self._process_response_body(response, settings=settings) zappa_returndict["body"] = processed_body if is_base64_encoded: zappa_returndict["isBase64Encoded"] = is_base64_encoded From 71c8aa36db8f97f325d12f4fb517b0a16308ea1c Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 12 Aug 2022 18:29:57 +0900 Subject: [PATCH 032/111] :wrench: Expand default text mimetypes mentioned in https://github.com/zappa/Zappa/pull/1023 :recycle: define "DEFAULT_TEXT_MIMETYPES" and move to utilities.py --- zappa/handler.py | 6 +++--- zappa/utilities.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 88976b4d7..920ad928f 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -20,11 +20,11 @@ # so handle both scenarios. try: from zappa.middleware import ZappaWSGIMiddleware - from zappa.utilities import merge_headers, parse_s3_url + from zappa.utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES from zappa.wsgi import common_log, create_wsgi_request except ImportError: # pragma: no cover from .middleware import ZappaWSGIMiddleware - from .utilities import merge_headers, parse_s3_url + from .utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES from .wsgi import common_log, create_wsgi_request @@ -286,7 +286,7 @@ def _process_response_body(response: Response, settings: ModuleType) -> Tuple[st """ encode_body_as_base64 = False if settings.BINARY_SUPPORT: - handle_as_text_mimetypes = ("text/", "application/json") + handle_as_text_mimetypes = DEFAULT_TEXT_MIMETYPES additional_text_mimetypes = getattr(settings, "ADDITIONAL_TEXT_MIMETYPES", None) if additional_text_mimetypes: handle_as_text_mimetypes += tuple(additional_text_mimetypes) diff --git a/zappa/utilities.py b/zappa/utilities.py index 72ad9f0f7..87a7fa3b3 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -20,6 +20,19 @@ # Settings / Packaging ## +# mimetypes starting with entries defined here are considered as TEXT when BINARTY_SUPPORT is True. +# - Additional TEXT mimetypes may be defined with the 'ADDITIONAL_TEXT_MIMETYPES' setting. +DEFAULT_TEXT_MIMETYPES = ( + "text/", + "application/json", # RFC 4627 + "application/javascript", # RFC 4329 + "application/ecmascript", # RFC 4329 + "application/xml", # RFC 3023 + "application/xml-external-parsed-entity", # RFC 3023 + "application/xml-dtd", # RFC 3023 + "image/svg+xml", # RFC 3023 +) + def copytree(src, dst, metadata=True, symlinks=False, ignore=None): """ From 48057fcb5ef3a5d31c48631dde31ef306181273a Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 12 Aug 2022 18:30:28 +0900 Subject: [PATCH 033/111] :art: run black/isort --- zappa/handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 920ad928f..1c6fdb0fd 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -20,11 +20,11 @@ # so handle both scenarios. try: from zappa.middleware import ZappaWSGIMiddleware - from zappa.utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES + from zappa.utilities import DEFAULT_TEXT_MIMETYPES, merge_headers, parse_s3_url from zappa.wsgi import common_log, create_wsgi_request except ImportError: # pragma: no cover from .middleware import ZappaWSGIMiddleware - from .utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES + from .utilities import DEFAULT_TEXT_MIMETYPES, merge_headers, parse_s3_url from .wsgi import common_log, create_wsgi_request From 1592c00a9f95a6dd93b4eb20e2ffb4d860660210 Mon Sep 17 00:00:00 2001 From: sha Date: Mon, 17 Oct 2022 17:51:33 +0800 Subject: [PATCH 034/111] wip --- README.md | 12 ++++++++++ test_settings.json | 6 ++++- tests/test_handler.py | 52 +++++++++++++++++++++++++++++++++++++++++++ zappa/cli.py | 24 ++++++++++++++++++++ zappa/core.py | 20 +++++++++++++++++ zappa/handler.py | 7 ++++++ 6 files changed, 120 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b9684219b..b6298df51 100644 --- a/README.md +++ b/README.md @@ -867,6 +867,18 @@ to change Zappa's behavior. Use these at your own risk! "assume_policy": "my_assume_policy.json", // optional, IAM assume policy JSON file "attach_policy": "my_attach_policy.json", // optional, IAM attach policy JSON file "apigateway_policy": "my_apigateway_policy.json", // optional, API Gateway resource policy JSON file + "function_url_enabled": false, // optional, set to true if you don't want to enable function URL. Default false. + "function_url_config": { + "authorizer": "NONE", // required if function url is enabled. default None. https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html + "cors": { + "allowedOrigins": [], // The origins that can access your function URL. default [*] + "allowedHeaders": [], // The HTTP headers that origins can include in requests to your function URL. + "allowedMethods": [], // The HTTP methods that are allowed when calling your function URL. For example: GET , POST , DELETE , or the wildcard character (* ). default [*] + "allowCredentials": false, //required, Whether to allow cookies or other credentials in requests to your function URL. default false. + "exposedResponseHeaders": [], The HTTP headers in your function response that you want to expose to origins that call your function URL. + "maxAge": 0 // The maximum amount of time, in seconds, that web browsers can cache results of a preflight request. default 0. + } + }, "async_source": "sns", // Source of async tasks. Defaults to "lambda" "async_resources": true, // Create the SNS topic and DynamoDB table to use. Defaults to true. "async_response_table": "your_dynamodb_table_name", // the DynamoDB table name to use for captured async responses; defaults to None (can't capture) diff --git a/test_settings.json b/test_settings.json index f7b29bfc2..97abfaf37 100644 --- a/test_settings.json +++ b/test_settings.json @@ -120,5 +120,9 @@ "lambda_concurrency_enabled": { "extends": "ttt888", "lambda_concurrency": 6 - } + }, + "function_url_enabled": { + "extends": "ttt888", + "function_url_enabled": true + } } diff --git a/tests/test_handler.py b/tests/test_handler.py index cc0590128..326b46be1 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -357,6 +357,58 @@ def test_wsgi_script_name_on_alb_event(self): "https://1234567890.execute-api.us-east-1.amazonaws.com/return/request/url", ) + def test_wsgi_script_name_on_function_url_event(self): + lh = LambdaHandler("tests.test_wsgi_script_name_settings") + + function_url_domain = "123456789.lambda-url.ap-southeast-1.on.aws" + + event = { + 'version': '2.0', + 'routeKey': '$default', + "rawPath": "/return/request/url", + 'rawQueryString': 'foo=bar', + + 'requestContext': { + 'accountId': 'anonymous', + 'apiId': 'no36vu2lg47wozii2hhzhrhkb40tjjgx', + 'domainName': function_url_domain, + 'domainPrefix': function_url_domain.split(".")[0], + 'http': { + 'method': 'GET', + 'path': '/return/request/url', + 'protocol': 'HTTP/1.1', + 'sourceIp': '202.161.35.23', + 'userAgent': 'curl/7.79.1' + }, + 'requestId': 'df63740e-e499-4356-9f5e-148056a5b42a', + 'routeKey': '$default', + 'stage': '$default', + 'time': '17/Oct/2022:06:52:18 +0000', + 'timeEpoch': 1665989538036 + }, + "queryStringParameters": {}, + "headers": { + "accept": "text/html,application/xhtml+xml", + "accept-language": "en-US,en;q=0.8", + "content-type": "text/plain", + "cookie": "cookies", + "host": function_url_domain, + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", + "x-amzn-trace-id": "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520", + "x-forwarded-for": "72.21.198.66", + "x-forwarded-port": "443", + "x-forwarded-proto": "https", + }, + "isBase64Encoded": False, + } + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertEqual( + response["body"], + f"https://{function_url_domain}/return/request/url", + ) + def test_merge_headers_no_multi_value(self): event = {"headers": {"a": "b"}} diff --git a/zappa/cli.py b/zappa/cli.py index 305e74e30..130384eb4 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -849,6 +849,14 @@ def deploy(self, source_zip=None, docker_image_uri=None): ) self.zappa.deploy_lambda_alb(**kwargs) + if self.use_function_url: + kwargs = dict( + lambda_arn=self.lambda_arn, + lambda_name=self.lambda_name, + function_url_config=self.function_url_config, + ) + self.zappa.deploy_lambda_function_url(**kwargs) + if self.use_apigateway: # Create and configure the API Gateway @@ -2295,6 +2303,22 @@ def load_settings(self, settings_file=None, session=None): self.use_alb = self.stage_config.get("alb_enabled", False) self.alb_vpc_config = self.stage_config.get("alb_vpc_config", {}) + # function URL settings + self.use_function_url = self.stage_config.get("function_url_enabled", True) + default_function_url_config = { + "authorizer": "NONE", + "cors": { + "allowedOrigins": ["*"], + "allowedHeaders": ["*"], + "allowedMethods": ["*"], + "allowCredentials": False, + "exposedResponseHeaders": ["*"], + "maxAge": 0 + } + } + self.function_url_config = self.stage_config.get("function_url_config", {}) + self.function_url_config.update(default_function_url_config) + # Additional tags self.tags = self.stage_config.get("tags", {}) diff --git a/zappa/core.py b/zappa/core.py index 6de97f429..e4464585b 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1461,6 +1461,26 @@ def delete_lambda_function(self, function_name): FunctionName=function_name, ) + ## + # Function URL + ## + def deploy_lambda_function_url(self, function_name, function_url_config): + response = self.lambda_client.create_function_url_config( + FunctionName=function_name, + Qualifier='string', + AuthType=function_url_config['authorizer'], + Cors={ + 'AllowCredentials': function_url_config["cors"]["allowCredentials"], + 'AllowHeaders': function_url_config["cors"]["allowedHeaders"], + 'AllowMethods': function_url_config["cors"]["allowedMethods"], + 'AllowOrigins': function_url_config["cors"]["allowedOrigins"], + 'ExposeHeaders': function_url_config["cors"]["exposedResponseHeaders"], + 'MaxAge': function_url_config["cors"]["maxAge"] + } + ) + print(f"function URL response {response}") + pass + ## # Application load balancer ## diff --git a/zappa/handler.py b/zappa/handler.py index ed0cc9835..5b32faa29 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -485,6 +485,13 @@ def handler(self, event, context): # Timing time_start = datetime.datetime.now() + # This is a function url request + if event.get("rawPath", None): + http_info = event['requestContext']['http'] + event['httpMethod'] = http_info['method'] + event['path'] = http_info['path'] + event['body'] = event.get("body", None) + # This is a normal HTTP request if event.get("httpMethod", None): script_name = "" From 0cb9871914d7183884490e136d99f17cf99e3cbd Mon Sep 17 00:00:00 2001 From: sha Date: Tue, 18 Oct 2022 18:10:52 +0800 Subject: [PATCH 035/111] deployment working. next: delete policy --- tests/test_handler.py | 39 +++++++++++++++-------------- zappa/cli.py | 26 +++++++++++++------- zappa/core.py | 57 ++++++++++++++++++++++++++++++++++--------- zappa/handler.py | 8 +++--- 4 files changed, 86 insertions(+), 44 deletions(-) diff --git a/tests/test_handler.py b/tests/test_handler.py index 326b46be1..d37fb681f 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -363,28 +363,27 @@ def test_wsgi_script_name_on_function_url_event(self): function_url_domain = "123456789.lambda-url.ap-southeast-1.on.aws" event = { - 'version': '2.0', - 'routeKey': '$default', + "version": "2.0", + "routeKey": "$default", "rawPath": "/return/request/url", - 'rawQueryString': 'foo=bar', - - 'requestContext': { - 'accountId': 'anonymous', - 'apiId': 'no36vu2lg47wozii2hhzhrhkb40tjjgx', - 'domainName': function_url_domain, - 'domainPrefix': function_url_domain.split(".")[0], - 'http': { - 'method': 'GET', - 'path': '/return/request/url', - 'protocol': 'HTTP/1.1', - 'sourceIp': '202.161.35.23', - 'userAgent': 'curl/7.79.1' + "rawQueryString": "foo=bar", + "requestContext": { + "accountId": "anonymous", + "apiId": "no36vu2lg47wozii2hhzhrhkb40tjjgx", + "domainName": function_url_domain, + "domainPrefix": function_url_domain.split(".")[0], + "http": { + "method": "GET", + "path": "/return/request/url", + "protocol": "HTTP/1.1", + "sourceIp": "202.161.35.23", + "userAgent": "curl/7.79.1", }, - 'requestId': 'df63740e-e499-4356-9f5e-148056a5b42a', - 'routeKey': '$default', - 'stage': '$default', - 'time': '17/Oct/2022:06:52:18 +0000', - 'timeEpoch': 1665989538036 + "requestId": "df63740e-e499-4356-9f5e-148056a5b42a", + "routeKey": "$default", + "stage": "$default", + "time": "17/Oct/2022:06:52:18 +0000", + "timeEpoch": 1665989538036, }, "queryStringParameters": {}, "headers": { diff --git a/zappa/cli.py b/zappa/cli.py index 130384eb4..a1628e0d0 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -851,8 +851,7 @@ def deploy(self, source_zip=None, docker_image_uri=None): if self.use_function_url: kwargs = dict( - lambda_arn=self.lambda_arn, - lambda_name=self.lambda_name, + function_name=self.lambda_arn, function_url_config=self.function_url_config, ) self.zappa.deploy_lambda_function_url(**kwargs) @@ -1117,6 +1116,15 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): else: endpoint_url = None + if self.use_function_url: + kwargs = dict( + function_name=self.lambda_arn, + function_url_config=self.function_url_config, + ) + self.zappa.update_lambda_function_url(**kwargs) + else: + self.zappa.delete_lambda_function_url(self.lambda_arn) + self.schedule() # Update any cognito pool with the lambda arn @@ -2308,13 +2316,13 @@ def load_settings(self, settings_file=None, session=None): default_function_url_config = { "authorizer": "NONE", "cors": { - "allowedOrigins": ["*"], - "allowedHeaders": ["*"], - "allowedMethods": ["*"], - "allowCredentials": False, - "exposedResponseHeaders": ["*"], - "maxAge": 0 - } + "allowedOrigins": ["*"], + "allowedHeaders": ["*"], + "allowedMethods": ["*"], + "allowCredentials": False, + "exposedResponseHeaders": ["*"], + "maxAge": 0, + }, } self.function_url_config = self.stage_config.get("function_url_config", {}) self.function_url_config.update(default_function_url_config) diff --git a/zappa/core.py b/zappa/core.py index e4464585b..dfb8b6d7b 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1465,21 +1465,54 @@ def delete_lambda_function(self, function_name): # Function URL ## def deploy_lambda_function_url(self, function_name, function_url_config): + response = self.lambda_client.create_function_url_config( FunctionName=function_name, - Qualifier='string', - AuthType=function_url_config['authorizer'], + AuthType=function_url_config["authorizer"], Cors={ - 'AllowCredentials': function_url_config["cors"]["allowCredentials"], - 'AllowHeaders': function_url_config["cors"]["allowedHeaders"], - 'AllowMethods': function_url_config["cors"]["allowedMethods"], - 'AllowOrigins': function_url_config["cors"]["allowedOrigins"], - 'ExposeHeaders': function_url_config["cors"]["exposedResponseHeaders"], - 'MaxAge': function_url_config["cors"]["maxAge"] - } + "AllowCredentials": function_url_config["cors"]["allowCredentials"], + "AllowHeaders": function_url_config["cors"]["allowedHeaders"], + "AllowMethods": function_url_config["cors"]["allowedMethods"], + "AllowOrigins": function_url_config["cors"]["allowedOrigins"], + "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], + "MaxAge": function_url_config["cors"]["maxAge"], + }, ) - print(f"function URL response {response}") - pass + print(f"function URL address: {response['FunctionUrl']}") + if function_url_config["authorizer"] == "NONE": + permission_response = self.lambda_client.add_permission( + FunctionName=function_name, + StatementId="FunctionURLAllowPublicAccess", + Action="lambda:InvokeFunction", + Principal="*", + ) + permission_response + + def update_lambda_function_url(self, function_name, function_url_config): + response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) + if response.get("FunctionUrlConfigs", []): + for config in response["FunctionUrlConfigs"]: + response = self.lambda_client.update_function_url_config( + FunctionName=config["FunctionArn"], + AuthType=function_url_config["authorizer"], + Cors={ + "AllowCredentials": function_url_config["cors"]["allowCredentials"], + "AllowHeaders": function_url_config["cors"]["allowedHeaders"], + "AllowMethods": function_url_config["cors"]["allowedMethods"], + "AllowOrigins": function_url_config["cors"]["allowedOrigins"], + "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], + "MaxAge": function_url_config["cors"]["maxAge"], + }, + ) + print(f"function URL address: {response['FunctionUrl']}") + else: + self.deploy_lambda_function_url(function_name, function_url_config) + + def delete_lambda_function_url(self, function_name): + response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) + for config in response.get("FunctionUrlConfigs", []): + resp = self.lambda_client.delete_function_url_config(FunctionName=config["FunctionArn"]) + print(f"function URL deleted: {config['FunctionUrl']}") ## # Application load balancer @@ -2748,6 +2781,8 @@ def _clear_policy(self, lambda_name): if policy_response["ResponseMetadata"]["HTTPStatusCode"] == 200: statement = json.loads(policy_response["Policy"])["Statement"] for s in statement: + if s["Sid"] in ["FunctionURLAllowPublicAccess"]: + continue delete_response = self.lambda_client.remove_permission(FunctionName=lambda_name, StatementId=s["Sid"]) if delete_response["ResponseMetadata"]["HTTPStatusCode"] != 204: logger.error("Failed to delete an obsolete policy statement: {}".format(policy_response)) diff --git a/zappa/handler.py b/zappa/handler.py index 5b32faa29..54ba71126 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -487,10 +487,10 @@ def handler(self, event, context): # This is a function url request if event.get("rawPath", None): - http_info = event['requestContext']['http'] - event['httpMethod'] = http_info['method'] - event['path'] = http_info['path'] - event['body'] = event.get("body", None) + http_info = event["requestContext"]["http"] + event["httpMethod"] = http_info["method"] + event["path"] = http_info["path"] + event["body"] = event.get("body", None) # This is a normal HTTP request if event.get("httpMethod", None): From 43f9e13033f98738dcc2279cf878e22a9f64c70a Mon Sep 17 00:00:00 2001 From: sha Date: Fri, 21 Oct 2022 14:37:59 +0800 Subject: [PATCH 036/111] added test cases --- tests/tests.py | 254 +++++++++++++++++++++++++++++++++++++++++++++++++ zappa/cli.py | 4 +- zappa/core.py | 59 +++++++++--- 3 files changed, 304 insertions(+), 13 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 589211684..2964c05a4 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -2828,6 +2828,260 @@ def test_delete_lambda_concurrency(self, client): FunctionName="abc", ) + @mock.patch("botocore.client") + def test_deploy_lambda_function_url(self, client): + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + function_name = "abc" + function_url_config = { + "authorizer": "NONE", + "cors": { + "allowedOrigins": ["*"], + "allowedHeaders": ["*"], + "allowedMethods": ["*"], + "allowCredentials": False, + "exposedResponseHeaders": ["*"], + "maxAge": 0, + }, + } + zappa_core.lambda_client.create_function_url_config.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 201, + "RetryAttempts": 0, + }, + "FunctionUrl": "https://xxxxx.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": "arn:aws:lambda:ap-southeast-1:123456789:function:{}".format(function_name), + } + zappa_core.lambda_client.add_permission.return_value = { + "ResponseMetadata": { + "RequestId": "cbe73d4e-007e-4476-a4a0-fbd07599570a", + "HTTPStatusCode": 201, + "RetryAttempts": 0, + }, + "Statement": '{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":"arn:aws:lambda:ap-southeast-1:123456789:function:abc"}', + } + + zappa_core.deploy_lambda_function_url(function_name="abc", function_url_config=function_url_config) + boto_mock.client().create_function_url_config.assert_called_with( + FunctionName=function_name, + AuthType=function_url_config["authorizer"], + Cors={ + "AllowCredentials": function_url_config["cors"]["allowCredentials"], + "AllowHeaders": function_url_config["cors"]["allowedHeaders"], + "AllowMethods": function_url_config["cors"]["allowedMethods"], + "AllowOrigins": function_url_config["cors"]["allowedOrigins"], + "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], + "MaxAge": function_url_config["cors"]["maxAge"], + }, + ) + + boto_mock.client().add_permission.assert_called_with( + FunctionName=function_name, + StatementId="FunctionURLAllowPublicAccess", + Action="lambda:InvokeFunction", + Principal="*", + ) + + @mock.patch("botocore.client") + def test_update_lambda_function_url(self, client): + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + function_name = "abc" + function_arn = "arn:aws:lambda:ap-southeast-1:123456789:function:{}".format(function_name) + function_url_config = { + "authorizer": "NONE", + "cors": { + "allowedOrigins": ["*"], + "allowedHeaders": ["*"], + "allowedMethods": ["*"], + "allowCredentials": False, + "exposedResponseHeaders": ["*"], + "maxAge": 0, + }, + } + zappa_core.lambda_client.list_function_url_configs.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FunctionUrlConfigs": [ + { + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": function_arn, + } + ], + } + zappa_core.lambda_client.update_function_url_config.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": function_arn, + } + zappa_core.lambda_client.get_policy.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', + } + + zappa_core.update_lambda_function_url(function_name="abc", function_url_config=function_url_config) + boto_mock.client().update_function_url_config.assert_called_with( + FunctionName=function_arn, + AuthType=function_url_config["authorizer"], + Cors={ + "AllowCredentials": function_url_config["cors"]["allowCredentials"], + "AllowHeaders": function_url_config["cors"]["allowedHeaders"], + "AllowMethods": function_url_config["cors"]["allowedMethods"], + "AllowOrigins": function_url_config["cors"]["allowedOrigins"], + "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], + "MaxAge": function_url_config["cors"]["maxAge"], + }, + ) + + boto_mock.client().get_policy.assert_called_with( + FunctionName=function_arn, + ) + boto_mock.client().add_permission.assert_not_called() + boto_mock.client().create_function_url_config.assert_not_called() + + @mock.patch("botocore.client") + def test_update_lambda_function_url_iam_authorizer(self, client): + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + function_name = "abc" + function_arn = "arn:aws:lambda:ap-southeast-1:123456789:function:{}".format(function_name) + function_url_config = { + "authorizer": "AWS_IAM", + "cors": { + "allowedOrigins": ["*"], + "allowedHeaders": ["*"], + "allowedMethods": ["*"], + "allowCredentials": False, + "exposedResponseHeaders": ["*"], + "maxAge": 0, + }, + } + zappa_core.lambda_client.list_function_url_configs.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FunctionUrlConfigs": [ + { + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": function_arn, + } + ], + } + zappa_core.lambda_client.update_function_url_config.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": function_arn, + } + zappa_core.lambda_client.get_policy.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', + } + zappa_core.lambda_client.remove_permission.return_value = { + "ResponseMetadata": {"HTTPStatusCode": 200}, + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":"xxxxx"}]}', + } + zappa_core.update_lambda_function_url(function_name="abc", function_url_config=function_url_config) + boto_mock.client().update_function_url_config.assert_called_with( + FunctionName=function_arn, + AuthType=function_url_config["authorizer"], + Cors={ + "AllowCredentials": function_url_config["cors"]["allowCredentials"], + "AllowHeaders": function_url_config["cors"]["allowedHeaders"], + "AllowMethods": function_url_config["cors"]["allowedMethods"], + "AllowOrigins": function_url_config["cors"]["allowedOrigins"], + "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], + "MaxAge": function_url_config["cors"]["maxAge"], + }, + ) + + boto_mock.client().get_policy.assert_called_with( + FunctionName=function_arn, + ) + boto_mock.client().delete_policy.remove_permission( + FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccess" + ) + boto_mock.client().add_permission.assert_not_called() + boto_mock.client().create_function_url_config.assert_not_called() + + @mock.patch("botocore.client") + def test_delete_lambda_function_url(self, client): + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + function_name = "abc" + function_arn = "arn:aws:lambda:ap-southeast-1:123456789:function:{}".format(function_name) + + zappa_core.lambda_client.list_function_url_configs.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FunctionUrlConfigs": [ + { + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": function_arn, + } + ], + } + zappa_core.lambda_client.delete_function_url_config.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 204, + } + } + zappa_core.lambda_client.get_policy.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', + } + zappa_core.lambda_client.remove_permission.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 204, + "RetryAttempts": 0, + } + } + zappa_core.delete_lambda_function_url(function_name=function_arn) + boto_mock.client().delete_function_url_config.assert_called_with( + FunctionName=function_arn, + ) + + boto_mock.client().get_policy.assert_called_with( + FunctionName=function_arn, + ) + boto_mock.client().delete_policy.remove_permission( + FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccess" + ) + boto_mock.client().add_permission.assert_not_called() + boto_mock.client().create_function_url_config.assert_not_called() + boto_mock.client().update_function_url_config.assert_not_called() + @mock.patch("sys.version_info", new_callable=get_unsupported_sys_versioninfo) def test_unsupported_version_error(self, *_): from importlib import reload diff --git a/zappa/cli.py b/zappa/cli.py index a1628e0d0..42fe58cc0 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -2324,8 +2324,8 @@ def load_settings(self, settings_file=None, session=None): "maxAge": 0, }, } - self.function_url_config = self.stage_config.get("function_url_config", {}) - self.function_url_config.update(default_function_url_config) + default_function_url_config.update(self.stage_config.get("function_url_config", {})) + self.function_url_config = default_function_url_config # Additional tags self.tags = self.stage_config.get("tags", {}) diff --git a/zappa/core.py b/zappa/core.py index dfb8b6d7b..b014d8fe4 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1464,6 +1464,46 @@ def delete_lambda_function(self, function_name): ## # Function URL ## + def list_function_url_policy(self, function_name): + results = [] + try: + policy_response = self.lambda_client.get_policy(FunctionName=function_name) + if policy_response["ResponseMetadata"]["HTTPStatusCode"] == 200: + statement = json.loads(policy_response["Policy"])["Statement"] + for s in statement: + if s["Sid"] in ["FunctionURLAllowPublicAccess"]: + results.append(s) + else: + logger.debug("Failed to load Lambda function policy: {}".format(policy_response)) + except ClientError as e: + if e.args[0].find("ResourceNotFoundException") > -1: + logger.debug("No policy found, must be first run.") + else: + logger.error("Unexpected client error {}".format(e.args[0])) + return results + + def delete_function_url_policy(self, function_name): + statements = self.list_function_url_policy(function_name) + for s in statements: + delete_response = self.lambda_client.remove_permission(FunctionName=function_name, StatementId=s["Sid"]) + if delete_response["ResponseMetadata"]["HTTPStatusCode"] != 204: + logger.error("Failed to delete an obsolete policy statement: {}".format(delete_response)) + + def update_function_url_policy(self, function_name, function_url_config): + statements = self.list_function_url_policy(function_name) + + if function_url_config["authorizer"] == "NONE": + if not statements: + permission_response = self.lambda_client.add_permission( + FunctionName=function_name, + StatementId="FunctionURLAllowPublicAccess", + Action="lambda:InvokeFunction", + Principal="*", + ) + elif function_url_config["authorizer"] == "AWS_IAM": + if statements: + self.delete_function_url_policy(function_name) + def deploy_lambda_function_url(self, function_name, function_url_config): response = self.lambda_client.create_function_url_config( @@ -1478,15 +1518,9 @@ def deploy_lambda_function_url(self, function_name, function_url_config): "MaxAge": function_url_config["cors"]["maxAge"], }, ) - print(f"function URL address: {response['FunctionUrl']}") - if function_url_config["authorizer"] == "NONE": - permission_response = self.lambda_client.add_permission( - FunctionName=function_name, - StatementId="FunctionURLAllowPublicAccess", - Action="lambda:InvokeFunction", - Principal="*", - ) - permission_response + print("function URL address: {}".format(response["FunctionUrl"])) + self.update_function_url_policy(function_name, function_url_config) + return response def update_lambda_function_url(self, function_name, function_url_config): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) @@ -1504,7 +1538,8 @@ def update_lambda_function_url(self, function_name, function_url_config): "MaxAge": function_url_config["cors"]["maxAge"], }, ) - print(f"function URL address: {response['FunctionUrl']}") + print("function URL address: {}".format(response["FunctionUrl"])) + self.update_function_url_policy(config["FunctionArn"], function_url_config) else: self.deploy_lambda_function_url(function_name, function_url_config) @@ -1512,7 +1547,9 @@ def delete_lambda_function_url(self, function_name): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) for config in response.get("FunctionUrlConfigs", []): resp = self.lambda_client.delete_function_url_config(FunctionName=config["FunctionArn"]) - print(f"function URL deleted: {config['FunctionUrl']}") + if resp["ResponseMetadata"]["HTTPStatusCode"] == 204: + print("function URL deleted: {}".format(config["FunctionUrl"])) + self.delete_function_url_policy(config["FunctionArn"]) ## # Application load balancer From dc7588230fad09331fb3671d67ce6a09ad8650ff Mon Sep 17 00:00:00 2001 From: sha Date: Fri, 21 Oct 2022 14:39:31 +0800 Subject: [PATCH 037/111] remove handler update --- tests/test_handler.py | 51 ------------------------------------------- zappa/handler.py | 7 ------ 2 files changed, 58 deletions(-) diff --git a/tests/test_handler.py b/tests/test_handler.py index d37fb681f..cc0590128 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -357,57 +357,6 @@ def test_wsgi_script_name_on_alb_event(self): "https://1234567890.execute-api.us-east-1.amazonaws.com/return/request/url", ) - def test_wsgi_script_name_on_function_url_event(self): - lh = LambdaHandler("tests.test_wsgi_script_name_settings") - - function_url_domain = "123456789.lambda-url.ap-southeast-1.on.aws" - - event = { - "version": "2.0", - "routeKey": "$default", - "rawPath": "/return/request/url", - "rawQueryString": "foo=bar", - "requestContext": { - "accountId": "anonymous", - "apiId": "no36vu2lg47wozii2hhzhrhkb40tjjgx", - "domainName": function_url_domain, - "domainPrefix": function_url_domain.split(".")[0], - "http": { - "method": "GET", - "path": "/return/request/url", - "protocol": "HTTP/1.1", - "sourceIp": "202.161.35.23", - "userAgent": "curl/7.79.1", - }, - "requestId": "df63740e-e499-4356-9f5e-148056a5b42a", - "routeKey": "$default", - "stage": "$default", - "time": "17/Oct/2022:06:52:18 +0000", - "timeEpoch": 1665989538036, - }, - "queryStringParameters": {}, - "headers": { - "accept": "text/html,application/xhtml+xml", - "accept-language": "en-US,en;q=0.8", - "content-type": "text/plain", - "cookie": "cookies", - "host": function_url_domain, - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", - "x-amzn-trace-id": "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520", - "x-forwarded-for": "72.21.198.66", - "x-forwarded-port": "443", - "x-forwarded-proto": "https", - }, - "isBase64Encoded": False, - } - response = lh.handler(event, None) - - self.assertEqual(response["statusCode"], 200) - self.assertEqual( - response["body"], - f"https://{function_url_domain}/return/request/url", - ) - def test_merge_headers_no_multi_value(self): event = {"headers": {"a": "b"}} diff --git a/zappa/handler.py b/zappa/handler.py index 54ba71126..ed0cc9835 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -485,13 +485,6 @@ def handler(self, event, context): # Timing time_start = datetime.datetime.now() - # This is a function url request - if event.get("rawPath", None): - http_info = event["requestContext"]["http"] - event["httpMethod"] = http_info["method"] - event["path"] = http_info["path"] - event["body"] = event.get("body", None) - # This is a normal HTTP request if event.get("httpMethod", None): script_name = "" From 0f6f1dfaec6bc333739599ded45170d32b894123 Mon Sep 17 00:00:00 2001 From: sha Date: Fri, 21 Oct 2022 14:45:26 +0800 Subject: [PATCH 038/111] Update tests.py added setting test --- tests/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index 2964c05a4..a9e158b59 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1207,6 +1207,12 @@ def test_load_settings__lambda_concurrency_enabled(self): zappa_cli.load_settings("test_settings.json") self.assertEqual(6, zappa_cli.stage_config["lambda_concurrency"]) + def test_load_settings__function_url_enabled(self): + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "function_url_enabled" + zappa_cli.load_settings("test_settings.json") + self.assertEqual(True, zappa_cli.stage_config["function_url_enabled"]) + def test_load_settings_yml(self): zappa_cli = ZappaCLI() zappa_cli.api_stage = "ttt888" From 58e221b629ac7a155ca652c981f1f018b8d2c3ce Mon Sep 17 00:00:00 2001 From: monkut Date: Sat, 22 Oct 2022 11:06:46 +0900 Subject: [PATCH 039/111] :art: run black/isort --- tests/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 0954141f9..725947c84 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -84,4 +84,3 @@ def get_unsupported_sys_versioninfo() -> tuple: """Mock used to test the python unsupported version testcase""" invalid_versioninfo = namedtuple("version_info", ["major", "minor", "micro", "releaselevel", "serial"]) return invalid_versioninfo(3, 6, 1, "final", 0) - From 0ac2a1eb26f1a25f3cf09ee96647d54c12684856 Mon Sep 17 00:00:00 2001 From: sha Date: Sat, 22 Oct 2022 14:05:30 +0800 Subject: [PATCH 040/111] fix test cases --- .../lambda.CreateFunctionUrlConfig_1.json | 11 +++++++++++ .../lambda.ListFunctionUrlConfigs_1.json | 10 ++++++++++ .../lambda.UpdateFunctionUrlConfig_1.json | 14 ++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 tests/placebo/TestZappa.test_cli_aws/lambda.CreateFunctionUrlConfig_1.json create mode 100644 tests/placebo/TestZappa.test_cli_aws/lambda.ListFunctionUrlConfigs_1.json create mode 100644 tests/placebo/TestZappa.test_cli_aws/lambda.UpdateFunctionUrlConfig_1.json diff --git a/tests/placebo/TestZappa.test_cli_aws/lambda.CreateFunctionUrlConfig_1.json b/tests/placebo/TestZappa.test_cli_aws/lambda.CreateFunctionUrlConfig_1.json new file mode 100644 index 000000000..3076e1fb0 --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/lambda.CreateFunctionUrlConfig_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 201, + "data": { + "ResponseMetadata": { + "HTTPStatusCode": 201, + "RetryAttempts": 0 + }, + "FunctionUrl": "https://xxxxx.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": "arn:aws:lambda:ap-southeast-1:123456789:function:dev" + } +} \ No newline at end of file diff --git a/tests/placebo/TestZappa.test_cli_aws/lambda.ListFunctionUrlConfigs_1.json b/tests/placebo/TestZappa.test_cli_aws/lambda.ListFunctionUrlConfigs_1.json new file mode 100644 index 000000000..3884b88e9 --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/lambda.ListFunctionUrlConfigs_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": { + "HTTPStatusCode": 200 + }, + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": "1111111" + } +} \ No newline at end of file diff --git a/tests/placebo/TestZappa.test_cli_aws/lambda.UpdateFunctionUrlConfig_1.json b/tests/placebo/TestZappa.test_cli_aws/lambda.UpdateFunctionUrlConfig_1.json new file mode 100644 index 000000000..097f7aa91 --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/lambda.UpdateFunctionUrlConfig_1.json @@ -0,0 +1,14 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": { + "HTTPStatusCode": 200 + }, + "FunctionUrlConfigs": [ + { + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": "1111111" + } + ] + } +} \ No newline at end of file From 69f3b71e18ceb85d26f9f38a5336876a1fe341ae Mon Sep 17 00:00:00 2001 From: Rehan Hawari Date: Sat, 15 Oct 2022 16:44:38 +0700 Subject: [PATCH 041/111] feat: implement handler for event with format version 2.0 --- tests/test_handler.py | 32 +++++++++ tests/tests.py | 152 ++++++++++++++++++++++++++++++++++++++++++ zappa/handler.py | 121 +++++++++++++++++++++++++++++++++ zappa/wsgi.py | 92 ++++++++++++++++--------- 4 files changed, 366 insertions(+), 31 deletions(-) diff --git a/tests/test_handler.py b/tests/test_handler.py index b8cb59fee..95134c9d6 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -577,3 +577,35 @@ def test_cloudwatch_subscription_event(self): response = lh.handler(event, None) self.assertEqual(response, True) + + def test_wsgi_script_name_on_v2_formatted_event(self): + """ + Ensure that requests with payload format version 2.0 succeed + """ + lh = LambdaHandler("tests.test_wsgi_script_name_settings") + + event = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "requestContext": { + "http": { + "method": "GET", + "path": "/return/request/url", + }, + }, + "isBase64Encoded": False, + "body": "", + "cookies": ["Cookie_1=Value1; Expires=21 Oct 2021 07:48 GMT", "Cookie_2=Value2; Max-Age=78000"], + } + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertEqual( + response["body"], + "https://1234567890.execute-api.us-east-1.amazonaws.com/dev/return/request/url", + ) diff --git a/tests/tests.py b/tests/tests.py index 563e158f5..abd47e73c 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1086,6 +1086,158 @@ def test_wsgi_from_apigateway_testbutton(self): response_tuple = collections.namedtuple("Response", ["status_code", "content"]) response = response_tuple(200, "hello") + def test_wsgi_from_v2_event(self): + event = { + "version": "2.0", + "routeKey": "ANY /{proxy+}", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "accept-language": "en-US,en;q=0.9", + "cache-control": "no-cache", + "content-length": "0", + "dnt": "1", + "host": "qw8klxioji.execute-api.eu-west-1.amazonaws.com", + "pragma": "no-cache", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", + "x-forwarded-for": "50.191.225.98", + "x-forwarded-port": "443", + "x-forwarded-proto": "https", + }, + "requestContext": { + "accountId": "724336686645", + "apiId": "qw8klxioji", + "domainName": "qw8klxioji.execute-api.eu-west-1.amazonaws.com", + "domainPrefix": "qw8klxioji", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "50.191.225.98", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", + }, + "requestId": "xTG4wqXdSQ0RHpA=", + "routeKey": "ANY /{proxy+}", + "stage": "$default", + "time": "16/Oct/2022:11:17:12 +0000", + "timeEpoch": 1665919032135, + }, + "pathParameters": {"proxy": ""}, + "isBase64Encoded": False, + } + environ = create_wsgi_request(event, event_version="2.0") + self.assertTrue(environ) + + def test_wsgi_from_v2_event_with_lambda_authorizer(self): + principal_id = "user|a1b2c3d4" + authorizer = {"lambda": {"bool": True, "key": "value", "number": 1, "principalId": principal_id}} + event = { + "version": "2.0", + "routeKey": "ANY /{proxy+}", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer 1232314343", + "content-length": "28", + "content-type": "application/json", + "host": "qw8klxioji.execute-api.eu-west-1.amazonaws.com", + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", + "x-forwarded-for": "50.191.225.98", + "x-forwarded-port": "443", + "x-forwarded-proto": "https", + }, + "requestContext": { + "accountId": "724336686645", + "apiId": "qw8klxioji", + "authorizer": authorizer, + "domainName": "qw8klxioji.execute-api.eu-west-1.amazonaws.com", + "domainPrefix": "qw8klxioji", + "http": { + "method": "POST", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "50.191.225.98", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", + }, + "requestId": "aJ6Rqi93zQ0GPng=", + "routeKey": "ANY /{proxy+}", + "stage": "$default", + "time": "17/Oct/2022:14:58:44 +0000", + "timeEpoch": 1666018724000, + }, + "pathParameters": {"proxy": ""}, + "body": "{'data':'0123456789'}", + "isBase64Encoded": False, + } + environ = create_wsgi_request(event, event_version="2.0") + self.assertEqual(environ["API_GATEWAY_AUTHORIZER"], authorizer) + self.assertEqual(environ["REMOTE_USER"], principal_id) + + def test_wsgi_from_v2_event_with_iam_authorizer(self): + user_arn = "arn:aws:sts::724336686645:assumed-role/SAMLUSER/user.name" + authorizer = { + "iam": { + "accessKey": "AWSACCESSKEYID", + "accountId": "724336686645", + "callerId": "KFDJSURSUC8FU3ITCWEDJ:user.name", + "cognitoIdentity": None, + "principalOrgId": "aws:PrincipalOrgID", + "userArn": user_arn, + "userId": "KFDJSURSUC8FU3ITCWEDJ:user.name", + } + } + event = { + "version": "2.0", + "routeKey": "ANY /{proxy+}", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "authorization": "AWS4-HMAC-SHA256 Credential=AWSACCESSKEYID/20221017/eu-west-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=foosignature", + "content-length": "17", + "content-type": "application/json", + "host": "qw8klxioji.execute-api.eu-west-1.amazonaws.com", + "user-agent": "python-requests/2.28.1", + "x-amz-content-sha256": "foobar", + "x-amz-date": "20221017T150616Z", + "x-amz-security-token": "footoken", + "x-forwarded-for": "50.191.225.98", + "x-forwarded-port": "443", + "x-forwarded-proto": "https", + }, + "requestContext": { + "accountId": "724336686645", + "apiId": "qw8klxioji", + "authorizer": authorizer, + "domainName": "qw8klxioji.execute-api.eu-west-1.amazonaws.com", + "domainPrefix": "qw8klxioji", + "http": { + "method": "POST", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "50.191.225.98", + "userAgent": "python-requests/2.28.1", + }, + "requestId": "aJ5ZZgeYiQ0Rz-A=", + "routeKey": "ANY /{proxy+}", + "stage": "$default", + "time": "17/Oct/2022:15:06:16 +0000", + "timeEpoch": 1666019176656, + }, + "pathParameters": {"proxy": ""}, + "body": "{'data': '12345'}", + "isBase64Encoded": False, + } + environ = create_wsgi_request(event, event_version="2.0") + self.assertEqual(environ["API_GATEWAY_AUTHORIZER"], authorizer) + self.assertEqual(environ["REMOTE_USER"], user_arn) + ## # Handler ## diff --git a/zappa/handler.py b/zappa/handler.py index 1c6fdb0fd..208011ded 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -523,6 +523,127 @@ def handler(self, event, context): logger.error("Cannot find a function to process the triggered event.") return result + # This is an HTTP-protocol API Gateway event or Lambda url event with payload format version 2.0 + elif "version" in event and event["version"] == "2.0": + try: + time_start = datetime.datetime.now() + + script_name = "" + host = event.get("headers", {}).get("host") + if host: + if "amazonaws.com" in host: + logger.debug("amazonaws found in host") + # The path provided in th event doesn't include the + # stage, so we must tell Flask to include the API + # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 + script_name = f"/{settings.API_STAGE}" + else: + # This is a test request sent from the AWS console + if settings.DOMAIN: + # Assume the requests received will be on the specified + # domain. No special handling is required + pass + else: + # Assume the requests received will be to the + # amazonaws.com endpoint, so tell Flask to include the + # API stage + script_name = f"/{settings.API_STAGE}" + + base_path = getattr(settings, "BASE_PATH", None) + environ = create_wsgi_request( + event, + script_name=script_name, + base_path=base_path, + trailing_slash=self.trailing_slash, + binary_support=settings.BINARY_SUPPORT, + context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS, + event_version="2.0", + ) + + # We are always on https on Lambda, so tell our wsgi app that. + environ["HTTPS"] = "on" + environ["wsgi.url_scheme"] = "https" + environ["lambda.context"] = context + environ["lambda.event"] = event + + # Execute the application + with Response.from_app(self.wsgi_app, environ) as response: + response_body = None + response_is_base_64_encoded = False + if response.data: + if ( + settings.BINARY_SUPPORT + and not response.mimetype.startswith("text/") + and response.mimetype != "application/json" + ): + response_body = base64.b64encode(response.data).decode("utf-8") + response_is_base_64_encoded = True + else: + response_body = response.get_data(as_text=True) + + response_status_code = response.status_code + + cookies = [] + response_headers = {} + for key, value in response.headers: + if key.lower() == "set-cookie": + cookies.append(value) + else: + if key in response_headers: + updated_value = f"{response_headers[key]},{value}" + response_headers[key] = updated_value + else: + response_headers[key] = value + + # Calculate the total response time, + # and log it in the Common Log format. + time_end = datetime.datetime.now() + delta = time_end - time_start + response_time_ms = delta.total_seconds() * 1000 + response.content = response.data + common_log(environ, response, response_time=response_time_ms) + + return { + "cookies": cookies, + "isBase64Encoded": response_is_base_64_encoded, + "statusCode": response_status_code, + "headers": response_headers, + "body": response_body, + } + except Exception as e: + # Print statements are visible in the logs either way + print(e) + exc_info = sys.exc_info() + message = ( + "An uncaught exception happened while servicing this request. " + "You can investigate this with the `zappa tail` command." + ) + + # If we didn't even build an app_module, just raise. + if not settings.DJANGO_SETTINGS: + try: + self.app_module + except NameError as ne: + message = "Failed to import module: {}".format(ne.message) + + # Call exception handler for unhandled exceptions + exception_handler = self.settings.EXCEPTION_HANDLER + self._process_exception( + exception_handler=exception_handler, + event=event, + context=context, + exception=e, + ) + + # Return this unspecified exception as a 500, using template that API Gateway expects. + content = collections.OrderedDict() + content["statusCode"] = 500 + body = {"message": message} + if settings.DEBUG: # only include traceback if debug is on. + body["traceback"] = traceback.format_exception(*exc_info) # traceback as a list for readability. + content["body"] = json.dumps(str(body), sort_keys=True, indent=4) + return content + # Normal web app flow try: # Timing diff --git a/zappa/wsgi.py b/zappa/wsgi.py index c1889c08f..4581d8aa6 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -19,31 +19,70 @@ def create_wsgi_request( binary_support=False, base_path=None, context_header_mappings={}, + event_version="1.0", ): """ Given some event_info via API Gateway, create and return a valid WSGI request environ. """ - method = event_info.get("httpMethod", None) - headers = merge_headers(event_info) or {} # Allow for the AGW console 'Test' button to work (Pull #735) - - # API Gateway and ALB both started allowing for multi-value querystring - # params in Nov. 2018. If there aren't multi-value params present, then - # it acts identically to 'queryStringParameters', so we can use it as a - # drop-in replacement. - # - # The one caveat here is that ALB will only include _one_ of - # queryStringParameters _or_ multiValueQueryStringParameters, which means - # we have to check for the existence of one and then fall back to the - # other. - - if "multiValueQueryStringParameters" in event_info: - query = event_info["multiValueQueryStringParameters"] - query_string = urlencode(query, doseq=True) if query else "" - else: + if event_version == "2.0": + # See the new format documentation + # here: https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + method = event_info["requestContext"]["http"]["method"] + headers = event_info["headers"] + if event_info.get("cookies"): + headers["cookie"] = "; ".join(event_info["cookies"]) + + path = urls.url_unquote(event_info["requestContext"]["http"]["path"]) + query = event_info.get("queryStringParameters", {}) query_string = urlencode(query) if query else "" - query_string = urls.url_unquote(query_string) + query_string = urls.url_unquote(query_string) + + # Systems calling the Lambda (other than API Gateway) may not provide the field requestContext + # Extract remote_user, authorizer if Authorizer is enabled + remote_user = None + authorizer = event_info["requestContext"].get("authorizer", None) + if authorizer: + if authorizer.get("lambda"): + # Need to add principalId manually to lambda authorizer response context + remote_user = authorizer["lambda"].get("principalId") + elif authorizer.get("iam"): + remote_user = authorizer["iam"].get("userArn") + else: + method = event_info.get("httpMethod", None) + headers = merge_headers(event_info) or {} # Allow for the AGW console 'Test' button to work (Pull #735) + + path = urls.url_unquote(event_info["path"]) + + # API Gateway and ALB both started allowing for multi-value querystring + # params in Nov. 2018. If there aren't multi-value params present, then + # it acts identically to 'queryStringParameters', so we can use it as a + # drop-in replacement. + # + # The one caveat here is that ALB will only include _one_ of + # queryStringParameters _or_ multiValueQueryStringParameters, which means + # we have to check for the existence of one and then fall back to the + # other. + + if "multiValueQueryStringParameters" in event_info: + query = event_info["multiValueQueryStringParameters"] + query_string = urlencode(query, doseq=True) if query else "" + else: + query = event_info.get("queryStringParameters", {}) + query_string = urlencode(query) if query else "" + query_string = urls.url_unquote(query_string) + + # Systems calling the Lambda (other than API Gateway) may not provide the field requestContext + # Extract remote_user, authorizer if Authorizer is enabled + remote_user = None + authorizer = None + if "requestContext" in event_info: + authorizer = event_info["requestContext"].get("authorizer", None) + if authorizer: + remote_user = authorizer.get("principalId") + elif event_info["requestContext"].get("identity"): + remote_user = event_info["requestContext"]["identity"].get("userArn") if context_header_mappings: for key, value in context_header_mappings.items(): @@ -68,12 +107,12 @@ def create_wsgi_request( encoded_body = event_info["body"] body = base64.b64decode(encoded_body) else: - body = event_info["body"] + body = event_info.get("body") if isinstance(body, str): body = body.encode("utf-8") else: - body = event_info["body"] + body = event_info.get("body") if isinstance(body, str): body = body.encode("utf-8") @@ -81,7 +120,6 @@ def create_wsgi_request( # https://github.com/Miserlou/Zappa/issues/1188 headers = titlecase_keys(headers) - path = urls.url_unquote(event_info["path"]) if base_path: script_name = "/" + base_path @@ -115,16 +153,8 @@ def create_wsgi_request( "wsgi.run_once": False, } - # Systems calling the Lambda (other than API Gateway) may not provide the field requestContext - # Extract remote_user, authorizer if Authorizer is enabled - remote_user = None - if "requestContext" in event_info: - authorizer = event_info["requestContext"].get("authorizer", None) - if authorizer: - remote_user = authorizer.get("principalId") - environ["API_GATEWAY_AUTHORIZER"] = authorizer - elif event_info["requestContext"].get("identity"): - remote_user = event_info["requestContext"]["identity"].get("userArn") + if authorizer: + environ["API_GATEWAY_AUTHORIZER"] = authorizer # Input processing if method in ["POST", "PUT", "PATCH", "DELETE"]: From b389c34bc77e7ad77dbecd754a4b1c9a653964e3 Mon Sep 17 00:00:00 2001 From: Rehan Hawari Date: Sun, 23 Oct 2022 00:24:31 +0700 Subject: [PATCH 042/111] refactor: getting processed response body from new method --- zappa/handler.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 208011ded..45a9af1e4 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -571,15 +571,7 @@ def handler(self, event, context): response_body = None response_is_base_64_encoded = False if response.data: - if ( - settings.BINARY_SUPPORT - and not response.mimetype.startswith("text/") - and response.mimetype != "application/json" - ): - response_body = base64.b64encode(response.data).decode("utf-8") - response_is_base_64_encoded = True - else: - response_body = response.get_data(as_text=True) + response_body, response_is_base_64_encoded = self._process_response_body(response, settings=settings) response_status_code = response.status_code From 8182b63542f6470f59014560aaa4db279a7fddeb Mon Sep 17 00:00:00 2001 From: sha Date: Wed, 26 Oct 2022 12:49:04 +0800 Subject: [PATCH 043/111] update permission configuration --- tests/tests.py | 5 +++-- zappa/core.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 3d853ab82..15584fbd1 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -2580,7 +2580,7 @@ def test_deploy_lambda_function_url(self, client): "HTTPStatusCode": 201, "RetryAttempts": 0, }, - "Statement": '{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":"arn:aws:lambda:ap-southeast-1:123456789:function:abc"}', + "Statement": '{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunctionUrl","Resource":"arn:aws:lambda:ap-southeast-1:123456789:function:abc"}, "Condition":{"StringEquals":{"lambda: FunctionUrlAuthType":"NONE"}}', } zappa_core.deploy_lambda_function_url(function_name="abc", function_url_config=function_url_config) @@ -2600,8 +2600,9 @@ def test_deploy_lambda_function_url(self, client): boto_mock.client().add_permission.assert_called_with( FunctionName=function_name, StatementId="FunctionURLAllowPublicAccess", - Action="lambda:InvokeFunction", + Action="lambda:InvokeFunctionUrl", Principal="*", + FunctionUrlAuthType=function_url_config["authorizer"], ) @mock.patch("botocore.client") diff --git a/zappa/core.py b/zappa/core.py index b014d8fe4..d39293b4d 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1491,14 +1491,15 @@ def delete_function_url_policy(self, function_name): def update_function_url_policy(self, function_name, function_url_config): statements = self.list_function_url_policy(function_name) - + if function_url_config["authorizer"] == "NONE": if not statements: permission_response = self.lambda_client.add_permission( FunctionName=function_name, StatementId="FunctionURLAllowPublicAccess", - Action="lambda:InvokeFunction", + Action="lambda:InvokeFunctionUrl", Principal="*", + FunctionUrlAuthType=function_url_config["authorizer"], ) elif function_url_config["authorizer"] == "AWS_IAM": if statements: From 2bd95b9a74f575afbb84cd46bbc7726d68ec5a42 Mon Sep 17 00:00:00 2001 From: sha Date: Wed, 26 Oct 2022 16:07:25 +0800 Subject: [PATCH 044/111] custom domains for function url --- README.md | 4 + test_settings.json | 5 + .../cloudfront.DeleteDistribution_1.json | 12 + .../cloudfront.UpdateDistribution_1.json | 162 ++++++++++++ .../route53.ChangeResourceRecordSets_1.json | 1 + tests/tests.py | 246 ++++++++++++++++++ zappa/cli.py | 115 ++++---- zappa/core.py | 228 ++++++++++++++-- 8 files changed, 703 insertions(+), 70 deletions(-) create mode 100644 tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json create mode 100644 tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json create mode 100644 tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json diff --git a/README.md b/README.md index b6298df51..c749dd6a5 100644 --- a/README.md +++ b/README.md @@ -453,6 +453,8 @@ Zappa can be deployed to custom domain names and subdomains with custom SSL cert Currently, the easiest of these to use are the AWS Certificate Manager certificates, as they are free, self-renewing, and require the least amount of work. +APIGateway and Lambda FunctionURL both support custom domains. FunctionURL is implemented via cloudfront distribution. Set `domain` for APIGateway and `function_url_domains` for FunctionURL. + Once configured as described below, all of these methods use the same command: $ zappa certify @@ -879,6 +881,8 @@ to change Zappa's behavior. Use these at your own risk! "maxAge": 0 // The maximum amount of time, in seconds, that web browsers can cache results of a preflight request. default 0. } }, + "function_url_cloudfront_config": {}, // see https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudfront.html#CloudFront.Client.create_distribution + "function_url_custom_domains": [], // a list of custom domains. "async_source": "sns", // Source of async tasks. Defaults to "lambda" "async_resources": true, // Create the SNS topic and DynamoDB table to use. Defaults to true. "async_response_table": "your_dynamodb_table_name", // the DynamoDB table name to use for captured async responses; defaults to None (can't capture) diff --git a/test_settings.json b/test_settings.json index 97abfaf37..993c6cc95 100644 --- a/test_settings.json +++ b/test_settings.json @@ -124,5 +124,10 @@ "function_url_enabled": { "extends": "ttt888", "function_url_enabled": true + }, + "function_url_custom_domain": { + "extends": "function_url_enabled", + "function_url_domains": ["test-lambda-function-url.example.com", "test-lambda-function-url-1.example.com"], + "route53_enabled": true } } diff --git a/tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json b/tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json new file mode 100644 index 000000000..ffbb4ac71 --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json @@ -0,0 +1,12 @@ +{ + "ResponseMetadata": { + "RequestId": "7f720a6c-9e5b-4080-b289-df07fe33c7fc", + "HTTPStatusCode": 204, + "HTTPHeaders": { + "x-amzn-requestid": "7f720a6c-9e5b-4080-b289-df07fe33c7fc", + "content-type": "text/xml", + "date": "Wed, 26 Oct 2022 06:16:00 GMT" + }, + "RetryAttempts": 0 + } +} \ No newline at end of file diff --git a/tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json b/tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json new file mode 100644 index 000000000..32ff81426 --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json @@ -0,0 +1,162 @@ +{ + "ResponseMetadata": { + "RequestId": "e9a681a2-2a82-4e8c-bab0-133e6d1fd04d", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "x-amzn-requestid": "e9a681a2-2a82-4e8c-bab0-133e6d1fd04d", + "etag": "EQZU30ULEIL60", + "content-type": "text/xml", + "content-length": "4148", + "date": "Wed, 26 Oct 2022 03:45:13 GMT" + }, + "RetryAttempts": 0 + }, + "ETag": "EQZU30ULEIL60", + "Distribution": { + "Id": "E1YIU775JNY3JV", + "ARN": "arn:aws:cloudfront::123456789:distribution/E1YIU775JNY3JV", + "Status": "InProgress", + "LastModifiedTime": "", + "InProgressInvalidationBatches": 0, + "DomainName": "dolayrplf7f1.cloudfront.net", + "ActiveTrustedSigners": { + "Enabled": false, + "Quantity": 0 + }, + "ActiveTrustedKeyGroups": { + "Enabled": false, + "Quantity": 0 + }, + "DistributionConfig": { + "CallerReference": "zappa-create-function-url-custom-domain", + "Aliases": { + "Quantity": 1, + "Items": [ + "test-lambda-function-url.example.com" + ] + }, + "DefaultRootObject": "", + "Origins": { + "Quantity": 1, + "Items": [ + { + "Id": "LambdaFunctionURL", + "DomainName": "wwvjk2tpuvrr457k3xt4kuryby0qmmzs.lambda-url.ap-southeast-1.on.aws", + "OriginPath": "", + "CustomHeaders": { + "Quantity": 1, + "Items": [ + { + "HeaderName": "CloudFront", + "HeaderValue": "CloudFront" + } + ] + }, + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginProtocolPolicy": "https-only", + "OriginSslProtocols": { + "Quantity": 1, + "Items": [ + "TLSv1" + ] + }, + "OriginReadTimeout": 60, + "OriginKeepaliveTimeout": 60 + }, + "ConnectionAttempts": 3, + "ConnectionTimeout": 10, + "OriginShield": { + "Enabled": false + }, + "OriginAccessControlId": "" + } + ] + }, + "OriginGroups": { + "Quantity": 0 + }, + "DefaultCacheBehavior": { + "TargetOriginId": "LambdaFunctionURL", + "TrustedSigners": { + "Enabled": false, + "Quantity": 0 + }, + "TrustedKeyGroups": { + "Enabled": false, + "Quantity": 0 + }, + "ViewerProtocolPolicy": "redirect-to-https", + "AllowedMethods": { + "Quantity": 7, + "Items": [ + "HEAD", + "DELETE", + "POST", + "GET", + "OPTIONS", + "PUT", + "PATCH" + ], + "CachedMethods": { + "Quantity": 3, + "Items": [ + "HEAD", + "GET", + "OPTIONS" + ] + } + }, + "SmoothStreaming": true, + "Compress": true, + "LambdaFunctionAssociations": { + "Quantity": 0 + }, + "FunctionAssociations": { + "Quantity": 0 + }, + "FieldLevelEncryptionId": "", + "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" + }, + "CacheBehaviors": { + "Quantity": 0 + }, + "CustomErrorResponses": { + "Quantity": 0 + }, + "Comment": "Lambda FunctionURL zappa-function-url-test-dev", + "Logging": { + "Enabled": false, + "IncludeCookies": false, + "Bucket": "", + "Prefix": "" + }, + "PriceClass": "PriceClass_100", + "Enabled": true, + "ViewerCertificate": { + "CloudFrontDefaultCertificate": false, + "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789:certificate/77bff5cb-03c7-4b11-ba8e-312e6f49a31f", + "SSLSupportMethod": "sni-only", + "MinimumProtocolVersion": "TLSv1.2_2021", + "Certificate": "arn:aws:acm:us-east-1:123456789:certificate/77bff5cb-03c7-4b11-ba8e-312e6f49a31f", + "CertificateSource": "acm" + }, + "Restrictions": { + "GeoRestriction": { + "RestrictionType": "none", + "Quantity": 0 + } + }, + "WebACLId": "", + "HttpVersion": "http2", + "IsIPV6Enabled": true + }, + "AliasICPRecordals": [ + { + "CNAME": "test-lambda-function-url.example.com", + "ICPRecordalStatus": "APPROVED" + } + ] + } +} \ No newline at end of file diff --git a/tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json b/tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json new file mode 100644 index 000000000..ff367525f --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json @@ -0,0 +1 @@ +{'ResponseMetadata': {'RequestId': 'ec2fc714-a609-43ac-977e-2a8612d131a3', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': 'ec2fc714-a609-43ac-977e-2a8612d131a3', 'content-type': 'text/xml', 'content-length': '282', 'date': 'Wed, 26 Oct 2022 04:02:46 GMT'}, 'RetryAttempts': 0}, 'ChangeInfo': {'Id': '/change/C06780612UZZBG9OMVN6', 'Status': 'PENDING', 'SubmittedAt': datetime.datetime(2022, 10, 26, 4, 2, 47, 57000, tzinfo=tzutc())}} \ No newline at end of file diff --git a/tests/tests.py b/tests/tests.py index 15584fbd1..60134169e 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -2800,6 +2800,252 @@ def test_delete_lambda_function_url(self, client): boto_mock.client().create_function_url_config.assert_not_called() boto_mock.client().update_function_url_config.assert_not_called() + def test_function_url_create_custom_domain(self): + cloud_front_response = { + "ResponseMetadata": { + "RequestId": "e4410b01-e391-45d4-abe8-4f86508e0619", + "HTTPStatusCode": 201, + "RetryAttempts": 0, + }, + "Location": "https://cloudfront.amazonaws.com/2020-05-31/distribution/E1YIU775JNY3JV", + "ETag": "E1YQ89D7I4GX4C", + "Distribution": { + "Id": "E1YIU775JNY3JV", + "ARN": "arn:aws:cloudfront::123456789:distribution/E1YIU775JNY3JV", + "Status": "InProgress", + "InProgressInvalidationBatches": 0, + "DomainName": "dolayrplf7f1.cloudfront.net", + "ActiveTrustedSigners": {"Enabled": False, "Quantity": 0}, + "ActiveTrustedKeyGroups": {"Enabled": False, "Quantity": 0}, + "DistributionConfig": { + "CallerReference": "zappa-create-function-url-custom-domain", + "Aliases": {"Quantity": 0}, + "DefaultRootObject": "", + "Origins": { + "Quantity": 1, + "Items": [ + { + "Id": "LambdaFunctionURL", + "DomainName": "wwvjk2tpuvrr457k3xt4kuryby0qmmzs.lambda-url.ap-southeast-1.on.aws", + "OriginPath": "", + "CustomHeaders": {"Quantity": 0}, + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginProtocolPolicy": "https-only", + "OriginSslProtocols": {"Quantity": 3, "Items": ["TLSv1", "TLSv1.1", "TLSv1.2"]}, + "OriginReadTimeout": 30, + "OriginKeepaliveTimeout": 5, + }, + "ConnectionAttempts": 3, + "ConnectionTimeout": 10, + "OriginShield": {"Enabled": False}, + "OriginAccessControlId": "", + } + ], + }, + "OriginGroups": {"Quantity": 0}, + "DefaultCacheBehavior": { + "TargetOriginId": "LambdaFunctionURL", + "TrustedSigners": {"Enabled": False, "Quantity": 0}, + "TrustedKeyGroups": {"Enabled": False, "Quantity": 0}, + "ViewerProtocolPolicy": "redirect-to-https", + "AllowedMethods": { + "Quantity": 7, + "Items": ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"], + "CachedMethods": {"Quantity": 3, "Items": ["HEAD", "GET", "OPTIONS"]}, + }, + "SmoothStreaming": False, + "Compress": True, + "LambdaFunctionAssociations": {"Quantity": 0}, + "FunctionAssociations": {"Quantity": 0}, + "FieldLevelEncryptionId": "", + "ForwardedValues": { + "QueryString": True, + "Cookies": {"Forward": "all"}, + "Headers": {"Quantity": 3, "Items": ["Authorization", "Accept", "x-api-key"]}, + "QueryStringCacheKeys": {"Quantity": 0}, + }, + }, + "CacheBehaviors": {"Quantity": 0}, + "CustomErrorResponses": {"Quantity": 0}, + "Comment": "Lambda FunctionURL zappa-function-url-test-dev", + "Logging": {"Enabled": False, "IncludeCookies": False, "Bucket": "", "Prefix": ""}, + "PriceClass": "PriceClass_100", + "Enabled": True, + "ViewerCertificate": { + "CloudFrontDefaultCertificate": True, + "SSLSupportMethod": "vip", + "MinimumProtocolVersion": "TLSv1", + "CertificateSource": "cloudfront", + }, + "Restrictions": {"GeoRestriction": {"RestrictionType": "none", "Quantity": 0}}, + "WebACLId": "", + "HttpVersion": "http2", + "IsIPV6Enabled": True, + }, + }, + } + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + function_name = "abc" + function_arn = "arn:aws:lambda:ap-southeast-1:123456789:function:{}".format(function_name) + function_domains = ["function-url-domain.example.com", "function-url-domain-1.example.com"] + cert_arn = "arn:aws:acm:us-east-1:123456789:certificate/77bff5cb-03c7-4b11-ba8e-312e6f49a31f" + cloudfront_config = {} + zappa_core.lambda_client.list_function_url_configs.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FunctionUrlConfigs": [ + { + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": function_arn, + } + ], + } + zappa_core.cloudfront_client.list_distributions.return_value ={ + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "DistributionList": { + "Items": [] + }, + + } + zappa_core.cloudfront_client.create_distribution.return_value = cloud_front_response + domains = zappa_core.update_lambda_function_url_domains( + function_arn, function_domains, cert_arn, cloudfront_config + ) + boto_mock.client().list_function_url_configs.assert_called_with( + FunctionName=function_arn, MaxItems=50 + ) + boto_mock.client().list_distributions.assert_called() + boto_mock.client().create_distribution.assert_called() + assert domains + + def test_function_url_update_custom_domain(self): + cloud_front_distribution ={ + "Id": "E1YIU775JNY3JV", + "ARN": "arn:aws:cloudfront::123456789:distribution/E1YIU775JNY3JV", + "Status": "InProgress", + "InProgressInvalidationBatches": 0, + "DomainName": "dolayrplf7f1.cloudfront.net", + "ActiveTrustedSigners": {"Enabled": False, "Quantity": 0}, + "ActiveTrustedKeyGroups": {"Enabled": False, "Quantity": 0}, + "Origins": { + "Quantity": 1, + "Items": [ + { + "Id": "LambdaFunctionURL", + "DomainName": "123456789.lambda-url.ap-southeast-1.on.aws", + } + ], + }, + "DistributionConfig": { + "CallerReference": "zappa-create-function-url-custom-domain", + "Aliases": {"Quantity": 0}, + "DefaultRootObject": "", + + "OriginGroups": {"Quantity": 0}, + "DefaultCacheBehavior": { + "TargetOriginId": "LambdaFunctionURL", + "TrustedSigners": {"Enabled": False, "Quantity": 0}, + "TrustedKeyGroups": {"Enabled": False, "Quantity": 0}, + "ViewerProtocolPolicy": "redirect-to-https", + "AllowedMethods": { + "Quantity": 7, + "Items": ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"], + "CachedMethods": {"Quantity": 3, "Items": ["HEAD", "GET", "OPTIONS"]}, + }, + "SmoothStreaming": False, + "Compress": True, + "LambdaFunctionAssociations": {"Quantity": 0}, + "FunctionAssociations": {"Quantity": 0}, + "FieldLevelEncryptionId": "", + "ForwardedValues": { + "QueryString": True, + "Cookies": {"Forward": "all"}, + "Headers": {"Quantity": 3, "Items": ["Authorization", "Accept", "x-api-key"]}, + "QueryStringCacheKeys": {"Quantity": 0}, + }, + }, + "CacheBehaviors": {"Quantity": 0}, + "CustomErrorResponses": {"Quantity": 0}, + "Comment": "Lambda FunctionURL zappa-function-url-test-dev", + "Logging": {"Enabled": False, "IncludeCookies": False, "Bucket": "", "Prefix": ""}, + "PriceClass": "PriceClass_100", + "Enabled": True, + "ViewerCertificate": { + "CloudFrontDefaultCertificate": True, + "SSLSupportMethod": "vip", + "MinimumProtocolVersion": "TLSv1", + "CertificateSource": "cloudfront", + }, + "Restrictions": {"GeoRestriction": {"RestrictionType": "none", "Quantity": 0}}, + "WebACLId": "", + "HttpVersion": "http2", + "IsIPV6Enabled": True, + }, + } + cloud_front_response = { + "ResponseMetadata": { + "RequestId": "e4410b01-e391-45d4-abe8-4f86508e0619", + "HTTPStatusCode": 200, + "RetryAttempts": 0, + }, + "Location": "https://cloudfront.amazonaws.com/2020-05-31/distribution/E1YIU775JNY3JV", + "ETag": "E1YQ89D7I4GX4C", + "Distribution": cloud_front_distribution, + } + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + function_name = "abc" + function_arn = "arn:aws:lambda:ap-southeast-1:123456789:function:{}".format(function_name) + function_domains = ["function-url-domain.example.com", "function-url-domain-1.example.com"] + cert_arn = "arn:aws:acm:us-east-1:123456789:certificate/77bff5cb-03c7-4b11-ba8e-312e6f49a31f" + cloudfront_config = {} + zappa_core.lambda_client.list_function_url_configs.return_value = { + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FunctionUrlConfigs": [ + { + "FunctionUrl": "https://123456789.lambda-url.ap-southeast-1.on.aws/", + "FunctionArn": function_arn, + } + ], + } + zappa_core.cloudfront_client.list_distributions.return_value ={ + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "DistributionList": { + "Items": [cloud_front_distribution] + }, + + } + zappa_core.cloudfront_client.update_distribution.return_value = cloud_front_response + domains = zappa_core.update_lambda_function_url_domains( + function_arn, function_domains, cert_arn, cloudfront_config + ) + boto_mock.client().list_function_url_configs.assert_called_with( + FunctionName=function_arn, MaxItems=50 + ) + boto_mock.client().list_distributions.assert_called() + boto_mock.client().update_distribution.assert_called() + assert domains + @mock.patch("sys.version_info", new_callable=get_unsupported_sys_versioninfo) def test_unsupported_version_error(self, *_): from importlib import reload diff --git a/zappa/cli.py b/zappa/cli.py index 42fe58cc0..38cf0fc8d 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1225,6 +1225,9 @@ def undeploy(self, no_confirm=False, remove_logs=False): if self.use_alb: self.zappa.undeploy_lambda_alb(self.lambda_name) + if self.function_url_domains: + self.zappa.undeploy_function_url_custom_domain(self.lambda_name) + if self.use_apigateway: if remove_logs: self.zappa.remove_api_gateway_logs(self.lambda_name) @@ -1976,7 +1979,7 @@ def certify(self, no_confirm=True, manual=False): Register or update a domain certificate for this env. """ - if not self.domain: + if not (self.domain or self.function_url_domains): raise ClickException( "Can't certify a domain without " + click.style("domain", fg="red", bold=True) + " configured!" ) @@ -2057,59 +2060,73 @@ def certify(self, no_confirm=True, manual=False): certificate_chain = f.read() click.echo("Certifying domain " + click.style(self.domain, fg="green", bold=True) + "..") + route53 = self.stage_config.get("route53_enabled", True) - # Get cert and update domain. - - # Let's Encrypt - if not cert_location and not cert_arn: - from .letsencrypt import get_cert_and_update_domain + # Get cert and update domain for api_gateway + if self.use_apigateway: + # Let's Encrypt + if not cert_location and not cert_arn: + from .letsencrypt import get_cert_and_update_domain - cert_success = get_cert_and_update_domain(self.zappa, self.lambda_name, self.api_stage, self.domain, manual) + cert_success = get_cert_and_update_domain(self.zappa, self.lambda_name, self.api_stage, self.domain, manual) - # Custom SSL / ACM - else: - route53 = self.stage_config.get("route53_enabled", True) - if not self.zappa.get_domain_name(self.domain, route53=route53): - dns_name = self.zappa.create_domain_name( - domain_name=self.domain, - certificate_name=self.domain + "-Zappa-Cert", - certificate_body=certificate_body, - certificate_private_key=certificate_private_key, - certificate_chain=certificate_chain, - certificate_arn=cert_arn, - lambda_name=self.lambda_name, - stage=self.api_stage, - base_path=base_path, - ) - if route53: - self.zappa.update_route53_records(self.domain, dns_name) - print( - "Created a new domain name with supplied certificate. " - "Please note that it can take up to 40 minutes for this domain to be " - "created and propagated through AWS, but it requires no further work on your part." - ) + # Custom SSL / ACM else: - self.zappa.update_domain_name( - domain_name=self.domain, - certificate_name=self.domain + "-Zappa-Cert", - certificate_body=certificate_body, - certificate_private_key=certificate_private_key, - certificate_chain=certificate_chain, - certificate_arn=cert_arn, - lambda_name=self.lambda_name, - stage=self.api_stage, - route53=route53, - base_path=base_path, - ) - cert_success = True + if not self.zappa.get_domain_name(self.domain, route53=route53): + dns_name = self.zappa.create_domain_name( + domain_name=self.domain, + certificate_name=self.domain + "-Zappa-Cert", + certificate_body=certificate_body, + certificate_private_key=certificate_private_key, + certificate_chain=certificate_chain, + certificate_arn=cert_arn, + lambda_name=self.lambda_name, + stage=self.api_stage, + base_path=base_path, + ) + if route53: + self.zappa.update_route53_records(self.domain, dns_name) + print( + "Created a new domain name with supplied certificate. " + "Please note that it can take up to 40 minutes for this domain to be " + "created and propagated through AWS, but it requires no further work on your part." + ) + else: + self.zappa.update_domain_name( + domain_name=self.domain, + certificate_name=self.domain + "-Zappa-Cert", + certificate_body=certificate_body, + certificate_private_key=certificate_private_key, + certificate_chain=certificate_chain, + certificate_arn=cert_arn, + lambda_name=self.lambda_name, + stage=self.api_stage, + route53=route53, + base_path=base_path, + ) - if cert_success: - click.echo("Certificate " + click.style("updated", fg="green", bold=True) + "!") - else: - click.echo(click.style("Failed", fg="red", bold=True) + " to generate or install certificate! :(") - click.echo("\n==============\n") - shamelessly_promote() + cert_success = True + if cert_success: + click.echo("Certificate " + click.style("updated", fg="green", bold=True) + "!") + else: + click.echo(click.style("Failed", fg="red", bold=True) + " to generate or install certificate! :(") + click.echo("\n==============\n") + shamelessly_promote() + + if self.use_function_url: + self.lambda_arn = self.zappa.get_lambda_function(function_name=self.lambda_name) + dns_name = self.zappa.update_lambda_function_url_domains( + self.lambda_arn, self.function_url_domains, cert_arn, self.function_url_cloudfront_config + ) + if route53: + for domain in self.function_url_domains: + self.zappa.update_route53_records(domain, dns_name) + print( + "Created a new domain name with supplied certificate. " + "Please note that it can take up to 40 minutes for this domain to be " + "created and propagated through AWS, but it requires no further work on your part." + ) ## # Shell @@ -2313,6 +2330,8 @@ def load_settings(self, settings_file=None, session=None): # function URL settings self.use_function_url = self.stage_config.get("function_url_enabled", True) + self.function_url_domains = self.stage_config.get("function_url_domains", []) + self.function_url_cloudfront_config = self.stage_config.get("function_url_cloudfront_config", {}) default_function_url_config = { "authorizer": "NONE", "cors": { diff --git a/zappa/core.py b/zappa/core.py index d39293b4d..8264714a7 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1,6 +1,7 @@ """ Zappa core library. You may also want to look at `cli.py` and `util.py`. """ +import datetime ## # Imports @@ -19,6 +20,7 @@ import tarfile import tempfile import time +import urllib import uuid import zipfile from builtins import bytes, int @@ -363,6 +365,7 @@ def __init__( self.dynamodb_client = self.boto_client("dynamodb") self.cognito_client = self.boto_client("cognito-idp") self.sts_client = self.boto_client("sts") + self.cloudfront_client = self.boto_client("cloudfront") self.tags = tags self.cf_template = troposphere.Template() @@ -1491,7 +1494,7 @@ def delete_function_url_policy(self, function_name): def update_function_url_policy(self, function_name, function_url_config): statements = self.list_function_url_policy(function_name) - + if function_url_config["authorizer"] == "NONE": if not statements: permission_response = self.lambda_client.add_permission( @@ -1553,6 +1556,109 @@ def delete_lambda_function_url(self, function_name): self.delete_function_url_policy(config["FunctionArn"]) ## + # Cloudfront distribution + ## + def update_lambda_function_url_domains(self, function_name, function_url_domains, certificate_arn, cloudfront_config): + response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) + if not response.get("FunctionUrlConfigs", []): + print("no function url configured on lambda, skip setting custom domains") + url = response["FunctionUrlConfigs"][0]["FunctionUrl"] + url = urllib.parse.urlparse(url) + + NULL_CONFIG = {"Quantity": 0, "Items": []} + config = { + "CallerReference": "zappa-create-function-url-custom-domain", + "Aliases": {"Quantity": len(function_url_domains), "Items": function_url_domains}, + "DefaultRootObject": "", + "Enabled": True, + "PriceClass": "PriceClass_100", + "HttpVersion": "http2", + "Comment": "Lambda FunctionURL {}".format(function_name.split(":")[-1]), + "Origins": { + "Quantity": 1, + "Items": [ + { + "Id": "LambdaFunctionURL", + "DomainName": url.hostname, + "OriginPath": "", + "CustomHeaders": { + "Quantity": 1, + "Items": [ + {"HeaderName": "CloudFront", "HeaderValue": "CloudFront"}, + ], + }, + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginProtocolPolicy": "https-only", + "OriginSslProtocols": {"Quantity": 1, "Items": ["TLSv1"]}, + "OriginReadTimeout": 60, + "OriginKeepaliveTimeout": 60, + }, + } + ], + }, + "CacheBehaviors": NULL_CONFIG, + "CustomErrorResponses": NULL_CONFIG, + "DefaultCacheBehavior": { + "TargetOriginId": "LambdaFunctionURL", + "ViewerProtocolPolicy": "redirect-to-https", + "Compress": True, + "SmoothStreaming": True, + "LambdaFunctionAssociations": NULL_CONFIG, + "FieldLevelEncryptionId": "", + "AllowedMethods": { + "Quantity": 7, + "Items": ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"], + "CachedMethods": {"Quantity": 3, "Items": ["HEAD", "GET", "OPTIONS"]}, + }, + "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", # no cache, details see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html + }, + "Logging": {"Enabled": False, "IncludeCookies": False, "Bucket": "", "Prefix": ""}, + "Restrictions": {"GeoRestriction": {"RestrictionType": "none", **NULL_CONFIG}}, + "WebACLId": "", + } + if certificate_arn: + config["ViewerCertificate"] = { + "ACMCertificateArn": certificate_arn, + "SSLSupportMethod": "sni-only", + "MinimumProtocolVersion": "TLSv1.2_2021", + } + + config.update(cloudfront_config) + distributions = self.cloudfront_client.list_distributions() + distributions = [ + item + for item in distributions["DistributionList"]["Items"] + if url.hostname in [origin["DomainName"] for origin in item["Origins"]["Items"]] + ] + + if not distributions: + response = self.cloudfront_client.create_distribution(DistributionConfig=config) + if response["ResponseMetadata"]["HTTPStatusCode"] == 201: + print( + "created cloudfront distribution for {}. It will take a while for the change to be deployed.".format( + function_url_domains + ) + ) + return response["Distribution"]["DomainName"] + else: + id = distributions[0]["Id"] + distribution = self.cloudfront_client.get_distribution(Id=id) + new_config = distribution["Distribution"]["DistributionConfig"] + new_config.update(config) + + response = self.cloudfront_client.update_distribution( + DistributionConfig=new_config, Id=id, IfMatch=distribution["ETag"] + ) + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + print( + "update cloudfront distribution for {}. It will take a while for the change to be deployed.".format( + function_url_domains + ) + ) + return response["Distribution"]["DomainName"] + # Application load balancer ## @@ -2196,6 +2302,41 @@ def get_rest_apis(self, project_name): continue yield api + def undeploy_function_url_custom_domain(self, lambda_name, domains=None): + + response = self.lambda_client.list_function_url_configs(FunctionName=lambda_name, MaxItems=50) + if not response.get("FunctionUrlConfigs", []): + print("no function url configured on lambda. skip delete custom domains") + url = response["FunctionUrlConfigs"][0]["FunctionUrl"] + url = urllib.parse.urlparse(url) + distributions = self.cloudfront_client.list_distributions() + distributions = [ + item + for item in distributions["DistributionList"]["Items"] + if url.hostname in [origin["DomainName"] for origin in item["Origins"]["Items"]] + ] + + for distribution in distributions: + id = distribution["Id"] + distribution = self.cloudfront_client.get_distribution(Id=id) + new_config = distribution["Distribution"]["DistributionConfig"] + new_config["Enabled"] = False + + response = self.cloudfront_client.update_distribution( + DistributionConfig=new_config, Id=id, IfMatch=distribution["ETag"] + ) + response + print("wait for distribution to be disabled.") + waiter = self.cloudfront_client.get_waiter("distribution_deployed") + waiter.wait(Id=id, WaiterConfig={"Delay": 20, "MaxAttempts": 20}) + delete_response = self.cloudfront_client.delete_distribution(Id=id, IfMatch=response["ETag"]) + if delete_response["ResponseMetadata"]["HTTPStatusCode"] == 204: + print("cloudfront distribution deleted") + + # attempt remove 53 record + for domain in distribution["Distribution"]["DistributionConfig"]["Aliases"].get("Items", []): + self.delete_route53_records(domain, distribution["Distribution"]["DomainName"]) + def undeploy_api_gateway(self, lambda_name, domain_name=None, base_path=None): """ Delete a deployed REST API Gateway. @@ -2557,17 +2698,19 @@ def create_domain_name( certificateName=certificate_name, certificateArn=certificate_arn, ) + api_id = self.get_api_id(lambda_name) + if not api_id: + raise LookupError("No API URL to certify found - did you deploy?") - api_id = self.get_api_id(lambda_name) - if not api_id: - raise LookupError("No API URL to certify found - did you deploy?") + self.apigateway_client.create_base_path_mapping( + domainName=domain_name, + basePath="" if base_path is None else base_path, + restApiId=api_id, + stage=stage, + ) - self.apigateway_client.create_base_path_mapping( - domainName=domain_name, - basePath="" if base_path is None else base_path, - restApiId=api_id, - stage=stage, - ) + if self.function_url_enabled: + pass return agw_response["distributionDomainName"] @@ -2611,6 +2754,41 @@ def update_route53_records(self, domain_name, dns_name): return response + def delete_route53_records(self, domain_name, dns_name): + """ + Updates Route53 Records following GW domain creation + """ + zone_id = self.get_hosted_zone_id_for_domain(domain_name) + + is_apex = self.route53.get_hosted_zone(Id=zone_id)["HostedZone"]["Name"][:-1] == domain_name + if is_apex: + record_set = { + "Name": domain_name, + "Type": "A", + "AliasTarget": { + "HostedZoneId": "Z2FDTNDATAQYW2", # This is a magic value that means "CloudFront" + "DNSName": dns_name, + "EvaluateTargetHealth": False, + }, + } + else: + record_set = { + "Name": domain_name, + "Type": "CNAME", + "ResourceRecords": [{"Value": dns_name}], + "TTL": 60, + } + + response = self.route53.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={"Changes": [{"Action": "DELETE", "ResourceRecordSet": record_set}]}, + ) + + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + print("removed route 53 record for {}".format(domain_name)) + + return response + def update_domain_name( self, domain_name, @@ -2623,6 +2801,8 @@ def update_domain_name( stage=None, route53=True, base_path=None, + use_apigateway=True, + use_function_url=False, ): """ This updates your certificate information for an existing domain, @@ -2649,19 +2829,23 @@ def update_domain_name( ) certificate_arn = acm_certificate["CertificateArn"] - self.update_domain_base_path_mapping(domain_name, lambda_name, stage, base_path) + if use_apigateway: + self.update_domain_base_path_mapping(domain_name, lambda_name, stage, base_path) - return self.apigateway_client.update_domain_name( - domainName=domain_name, - patchOperations=[ - { - "op": "replace", - "path": "/certificateName", - "value": certificate_name, - }, - {"op": "replace", "path": "/certificateArn", "value": certificate_arn}, - ], - ) + res = self.apigateway_client.update_domain_name( + domainName=domain_name, + patchOperations=[ + { + "op": "replace", + "path": "/certificateName", + "value": certificate_name, + }, + {"op": "replace", "path": "/certificateArn", "value": certificate_arn}, + ], + ) + if use_function_url: + pass + return res def update_domain_base_path_mapping(self, domain_name, lambda_name, stage, base_path): """ From 8bf542d4bc333c4fc01654a40c33d33ef3a8608e Mon Sep 17 00:00:00 2001 From: sha Date: Tue, 1 Nov 2022 10:44:02 +0800 Subject: [PATCH 045/111] default function_url_enabled to false --- README.md | 2 +- zappa/cli.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b6298df51..687ae7785 100644 --- a/README.md +++ b/README.md @@ -867,7 +867,7 @@ to change Zappa's behavior. Use these at your own risk! "assume_policy": "my_assume_policy.json", // optional, IAM assume policy JSON file "attach_policy": "my_attach_policy.json", // optional, IAM attach policy JSON file "apigateway_policy": "my_apigateway_policy.json", // optional, API Gateway resource policy JSON file - "function_url_enabled": false, // optional, set to true if you don't want to enable function URL. Default false. + "function_url_enabled": false, // optional, set to true if you want to enable function URL. Default false. "function_url_config": { "authorizer": "NONE", // required if function url is enabled. default None. https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html "cors": { diff --git a/zappa/cli.py b/zappa/cli.py index 42fe58cc0..2e9711d18 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -2312,7 +2312,8 @@ def load_settings(self, settings_file=None, session=None): self.alb_vpc_config = self.stage_config.get("alb_vpc_config", {}) # function URL settings - self.use_function_url = self.stage_config.get("function_url_enabled", True) + self.use_function_url = self.stage_config.get("function_url_enabled", False) + default_function_url_config = { "authorizer": "NONE", "cors": { From a5b67bd3b40e295f3894800724fc4a2be3f19530 Mon Sep 17 00:00:00 2001 From: Sha Date: Sat, 3 Dec 2022 14:25:31 +0800 Subject: [PATCH 046/111] Update __init__.py handle FileNotFoundError: [Errno 2] No such file or directory: '/proc/1/cgroup' --- zappa/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 91ed01e5a..07c6333cc 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -8,9 +8,12 @@ def running_in_docker() -> bool: - When docker is used allow usage of any python version """ # https://stackoverflow.com/a/20012536/24718 - cgroup_content = Path("/proc/1/cgroup").read_text() - in_docker = "/docker/" in cgroup_content or "/lxc/" in cgroup_content - return in_docker + try: + cgroup_content = Path("/proc/1/cgroup").read_text() + in_docker = "/docker/" in cgroup_content or "/lxc/" in cgroup_content + return in_docker + except: + return False SUPPORTED_VERSIONS = [(3, 7), (3, 8), (3, 9)] From 46da48ad07369ea232e4fde61c3419e4c6c4beac Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 21 Dec 2022 13:20:08 +0800 Subject: [PATCH 047/111] Update core.py fix linting error --- zappa/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index fa9cd9542..67b47f26a 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1491,10 +1491,10 @@ def delete_function_url_policy(self, function_name): def update_function_url_policy(self, function_name, function_url_config): statements = self.list_function_url_policy(function_name) - + if function_url_config["authorizer"] == "NONE": if not statements: - permission_response = self.lambda_client.add_permission( + self.lambda_client.add_permission( FunctionName=function_name, StatementId="FunctionURLAllowPublicAccess", Action="lambda:InvokeFunctionUrl", From d6b831f1cffcca1f74167fda4856b7c0806eda16 Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 30 May 2023 16:16:18 +0800 Subject: [PATCH 048/111] fix test cases --- tests/test_wsgi_script_name_settings.py | 1 + zappa/wsgi.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_wsgi_script_name_settings.py b/tests/test_wsgi_script_name_settings.py index d2ec45cd9..750810a14 100644 --- a/tests/test_wsgi_script_name_settings.py +++ b/tests/test_wsgi_script_name_settings.py @@ -10,3 +10,4 @@ LOG_LEVEL = "DEBUG" PROJECT_NAME = "wsgi_script_name_settings" COGNITO_TRIGGER_MAPPING = {} +EXCEPTION_HANDLER = None \ No newline at end of file diff --git a/zappa/wsgi.py b/zappa/wsgi.py index c32786125..fd54dd2ea 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -3,6 +3,7 @@ import sys from io import BytesIO from urllib.parse import unquote, urlencode +from werkzeug import urls from .utilities import ApacheNCSAFormatter, merge_headers, titlecase_keys @@ -118,7 +119,6 @@ def create_wsgi_request( # https://github.com/Miserlou/Zappa/issues/1188 headers = titlecase_keys(headers) - path = unquote(event_info["path"]) if base_path: script_name = "/" + base_path From bc9cb6e60e17ee0c8589cfb869ee17bb53972ef6 Mon Sep 17 00:00:00 2001 From: Denny Biasiolli Date: Fri, 28 Jul 2023 09:00:03 +0200 Subject: [PATCH 049/111] adding support for Python 3.11 Ref. https://github.com/aws/aws-lambda-base-images/issues/62 --- .github/ISSUE_TEMPLATE.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/cd.yaml | 4 ++-- .github/workflows/ci.yaml | 2 +- README.md | 6 +++--- setup.py | 1 + tests/tests.py | 27 +++++++++++++++++++++++++++ zappa/__init__.py | 2 +- zappa/core.py | 4 +++- zappa/utilities.py | 4 +++- 10 files changed, 43 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d1fd903f5..cc4114a92 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ ## Context - + ## Expected Behavior diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a46c8a28c..79e454b3d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,7 @@ Before you submit this PR, please make sure that you meet these criteria: * Did you **make sure this code actually works on Lambda**, as well as locally? -* Did you test this code with all of **Python 3.7**, **Python 3.8**, **Python 3.9** and **Python 3.10** ? +* Did you test this code with all of **Python 3.7**, **Python 3.8**, **Python 3.9**, **Python 3.10** and **Python 3.11** ? * Does this commit ONLY relate to the issue at hand and have your linter shit all over the code? diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index b22f50413..b19664b48 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout Code Repository uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Install `pypa/build` run: python -m pip install build - name: Build sdist and wheel diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9f1177bfe..acb688536 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python: [3.7, 3.8, 3.9, "3.10"] + python: [3.7, 3.8, 3.9, "3.10", "3.11"] steps: - name: Checkout Code Repository uses: actions/checkout@v3 diff --git a/README.md b/README.md index 9af47f016..6c801b0ec 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ __Awesome!__ ## Installation and Configuration -_Before you begin, make sure you are running Python 3.7/3.8/3.9/3.10 and you have a valid AWS account and your [AWS credentials file](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) is properly installed._ +_Before you begin, make sure you are running Python 3.7/3.8/3.9/3.10/3.11 and you have a valid AWS account and your [AWS credentials file](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) is properly installed._ **Zappa** can easily be installed through pip, like so: @@ -443,7 +443,7 @@ For instance, suppose you have a basic application in a file called "my_app.py", Any remote print statements made and the value the function returned will then be printed to your local console. **Nifty!** -You can also invoke interpretable Python 3.7/3.8/3.9/3.10 strings directly by using `--raw`, like so: +You can also invoke interpretable Python 3.7/3.8/3.9/3.10/3.11 strings directly by using `--raw`, like so: $ zappa invoke production "print(1 + 2 + 3)" --raw @@ -984,7 +984,7 @@ to change Zappa's behavior. Use these at your own risk! "role_name": "MyLambdaRole", // Name of Zappa execution role. Default --ZappaExecutionRole. To use a different, pre-existing policy, you must also set manage_roles to false. "role_arn": "arn:aws:iam::12345:role/app-ZappaLambdaExecutionRole", // ARN of Zappa execution role. Default to None. To use a different, pre-existing policy, you must also set manage_roles to false. This overrides role_name. Use with temporary credentials via GetFederationToken. "route53_enabled": true, // Have Zappa update your Route53 Hosted Zones when certifying with a custom domain. Default true. - "runtime": "python3.10", // Python runtime to use on Lambda. Can be one of "python3.7", "python3.8", "python3.9", or "python3.10". Defaults to whatever the current Python being used is. + "runtime": "python3.11", // Python runtime to use on Lambda. Can be one of "python3.7", "python3.8", "python3.9", or "python3.10", or "python3.11". Defaults to whatever the current Python being used is. "s3_bucket": "dev-bucket", // Zappa zip bucket, "slim_handler": false, // Useful if project >50M. Set true to just upload a small handler to Lambda and load actual project from S3 at runtime. Default false. "settings_file": "~/Projects/MyApp/settings/dev_settings.py", // Server side settings file location, diff --git a/setup.py b/setup.py index e68926ac7..a0eb02667 100755 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Framework :: Django", "Framework :: Django :: 1.11", "Framework :: Django :: 2.0", diff --git a/tests/tests.py b/tests/tests.py index 07a38c41d..69312fc4b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -197,6 +197,33 @@ def test_get_manylinux_python310(self): self.assertTrue(os.path.isfile(path)) os.remove(path) + def test_get_manylinux_python311(self): + z = Zappa(runtime="python3.11") + self.assertIsNotNone(z.get_cached_manylinux_wheel("psycopg2-binary", "2.9.1")) + self.assertIsNone(z.get_cached_manylinux_wheel("derp_no_such_thing", "0.0")) + + # mock with a known manylinux wheel package so that code for downloading them gets invoked + mock_installed_packages = {"psycopg2-binary": "2.9.1"} + with mock.patch( + "zappa.core.Zappa.get_installed_packages", + return_value=mock_installed_packages, + ): + z = Zappa(runtime="python3.11") + path = z.create_lambda_zip(handler_file=os.path.realpath(__file__)) + self.assertTrue(os.path.isfile(path)) + os.remove(path) + + # same, but with an ABI3 package + mock_installed_packages = {"cryptography": "2.8"} + with mock.patch( + "zappa.core.Zappa.get_installed_packages", + return_value=mock_installed_packages, + ): + z = Zappa(runtime="python3.11") + path = z.create_lambda_zip(handler_file=os.path.realpath(__file__)) + self.assertTrue(os.path.isfile(path)) + os.remove(path) + def test_getting_installed_packages(self, *args): z = Zappa(runtime="python3.7") diff --git a/zappa/__init__.py b/zappa/__init__.py index 7918f71e8..402cdaab5 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -12,7 +12,7 @@ def running_in_docker() -> bool: return running_in_docker_flag -SUPPORTED_VERSIONS = [(3, 7), (3, 8), (3, 9), (3, 10)] +SUPPORTED_VERSIONS = [(3, 7), (3, 8), (3, 9), (3, 10), (3, 11)] MINIMUM_SUPPORTED_MINOR_VERSION = 7 if not running_in_docker() and sys.version_info[:2] not in SUPPORTED_VERSIONS: diff --git a/zappa/core.py b/zappa/core.py index b01a03a9d..4a4d8b616 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -314,8 +314,10 @@ def __init__( self.manylinux_suffix_start = "cp38" elif self.runtime == "python3.9": self.manylinux_suffix_start = "cp39" - else: + elif self.runtime == "python3.10": self.manylinux_suffix_start = "cp310" + else: + self.manylinux_suffix_start = "cp311" # AWS Lambda supports manylinux1/2010, manylinux2014, and manylinux_2_24 manylinux_suffixes = ("_2_24", "2014", "2010", "1") diff --git a/zappa/utilities.py b/zappa/utilities.py index b1da8474c..7cd11d1c5 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -214,8 +214,10 @@ def get_runtime_from_python_version(): return "python3.8" elif sys.version_info[1] <= 9: return "python3.9" - else: + elif sys.version_info[1] <= 10: return "python3.10" + else: + return "python3.11" ## From c1053f1a2cdfb2f552efbc5a9f8550c55a0436b5 Mon Sep 17 00:00:00 2001 From: Sha Date: Thu, 10 Aug 2023 09:19:28 +0800 Subject: [PATCH 050/111] version bump --- zappa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 402cdaab5..aa9ae36f0 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -30,4 +30,4 @@ def running_in_docker() -> bool: ) raise RuntimeError(err_msg) -__version__ = "0.57.0" +__version__ = "0.57.1" From cc396d545df09b4edf73fcf10dd6b52a8870ebad Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 30 Aug 2023 15:14:29 +0800 Subject: [PATCH 051/111] Update cli.py --- zappa/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/cli.py b/zappa/cli.py index 57e4c8321..09b1c6c8f 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1346,7 +1346,7 @@ def schedule(self): def unschedule(self): """ - Given a a list of scheduled functions, + Given a list of scheduled functions, tear down their regular execution. """ From 140594f6f059099364c678c52affe23298df8026 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 30 Aug 2023 16:18:20 +0800 Subject: [PATCH 052/111] make cors optional --- README.md | 2 +- zappa/core.py | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5f6372ecf..fed1a27c8 100644 --- a/README.md +++ b/README.md @@ -890,7 +890,7 @@ to change Zappa's behavior. Use these at your own risk! "function_url_enabled": false, // optional, set to true if you want to enable function URL. Default false. "function_url_config": { "authorizer": "NONE", // required if function url is enabled. default None. https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html - "cors": { + "cors": { // set to false if disable cors. "allowedOrigins": [], // The origins that can access your function URL. default [*] "allowedHeaders": [], // The HTTP headers that origins can include in requests to your function URL. "allowedMethods": [], // The HTTP methods that are allowed when calling your function URL. For example: GET , POST , DELETE , or the wildcard character (* ). default [*] diff --git a/zappa/core.py b/zappa/core.py index 43b7db796..46e0723f3 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1528,18 +1528,24 @@ def update_function_url_policy(self, function_name, function_url_config): def deploy_lambda_function_url(self, function_name, function_url_config): - response = self.lambda_client.create_function_url_config( - FunctionName=function_name, - AuthType=function_url_config["authorizer"], - Cors={ - "AllowCredentials": function_url_config["cors"]["allowCredentials"], - "AllowHeaders": function_url_config["cors"]["allowedHeaders"], - "AllowMethods": function_url_config["cors"]["allowedMethods"], - "AllowOrigins": function_url_config["cors"]["allowedOrigins"], - "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], - "MaxAge": function_url_config["cors"]["maxAge"], - }, - ) + if function_url_config["cors"]: + response = self.lambda_client.create_function_url_config( + FunctionName=function_name, + AuthType=function_url_config["authorizer"], + Cors={ + "AllowCredentials": function_url_config["cors"]["allowCredentials"], + "AllowHeaders": function_url_config["cors"]["allowedHeaders"], + "AllowMethods": function_url_config["cors"]["allowedMethods"], + "AllowOrigins": function_url_config["cors"]["allowedOrigins"], + "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], + "MaxAge": function_url_config["cors"]["maxAge"], + }, + ) + else: + response = self.lambda_client.create_function_url_config( + FunctionName=function_name, + AuthType=function_url_config["authorizer"] + ) print("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(function_name, function_url_config) return response From 1ea7c52278fa0e5251ed6b79587e656977341b99 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 30 Aug 2023 17:12:17 +0800 Subject: [PATCH 053/111] Update core.py --- zappa/core.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 46e0723f3..bd54c31cd 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1554,18 +1554,24 @@ def update_lambda_function_url(self, function_name, function_url_config): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) if response.get("FunctionUrlConfigs", []): for config in response["FunctionUrlConfigs"]: - response = self.lambda_client.update_function_url_config( - FunctionName=config["FunctionArn"], - AuthType=function_url_config["authorizer"], - Cors={ - "AllowCredentials": function_url_config["cors"]["allowCredentials"], - "AllowHeaders": function_url_config["cors"]["allowedHeaders"], - "AllowMethods": function_url_config["cors"]["allowedMethods"], - "AllowOrigins": function_url_config["cors"]["allowedOrigins"], - "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], - "MaxAge": function_url_config["cors"]["maxAge"], - }, - ) + if function_url_config["cors"]: + response = self.lambda_client.update_function_url_config( + FunctionName=config["FunctionArn"], + AuthType=function_url_config["authorizer"], + Cors={ + "AllowCredentials": function_url_config["cors"]["allowCredentials"], + "AllowHeaders": function_url_config["cors"]["allowedHeaders"], + "AllowMethods": function_url_config["cors"]["allowedMethods"], + "AllowOrigins": function_url_config["cors"]["allowedOrigins"], + "ExposeHeaders": function_url_config["cors"]["exposedResponseHeaders"], + "MaxAge": function_url_config["cors"]["maxAge"], + }, + ) + else: + response = self.lambda_client.create_function_url_config( + FunctionName=function_name, + AuthType=function_url_config["authorizer"] + ) print("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(config["FunctionArn"], function_url_config) else: From 57dfc74c5d4823efb34dbaa5ec09dd0aac1b408e Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 30 Aug 2023 17:18:41 +0800 Subject: [PATCH 054/111] Update core.py --- zappa/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/core.py b/zappa/core.py index bd54c31cd..4f1de78d6 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1568,7 +1568,7 @@ def update_lambda_function_url(self, function_name, function_url_config): }, ) else: - response = self.lambda_client.create_function_url_config( + response = self.lambda_client.update_function_url_config( FunctionName=function_name, AuthType=function_url_config["authorizer"] ) From a570f7e9cbac32ad1093b1d5831b014be02b221a Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 3 Oct 2023 14:41:01 +0800 Subject: [PATCH 055/111] Update wsgi.py --- zappa/wsgi.py | 95 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/zappa/wsgi.py b/zappa/wsgi.py index 9f3051f04..44356854f 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -18,34 +18,70 @@ def create_wsgi_request( binary_support=False, base_path=None, context_header_mappings={}, + event_version="1.0", ): """ Given some event_info via API Gateway, create and return a valid WSGI request environ. """ - method = event_info.get("httpMethod", None) - headers = merge_headers(event_info) or {} # Allow for the AGW console 'Test' button to work (Pull #735) - - # API Gateway and ALB both started allowing for multi-value querystring - # params in Nov. 2018. If there aren't multi-value params present, then - # it acts identically to 'queryStringParameters', so we can use it as a - # drop-in replacement. - # - # The one caveat here is that ALB will only include _one_ of - # queryStringParameters _or_ multiValueQueryStringParameters, which means - # we have to check for the existence of one and then fall back to the - # other. - - # Assumes that the lambda event provides the unencoded string as - # the value in "queryStringParameters"/"multiValueQueryStringParameters" - # The QUERY_STRING value provided to WSGI expects the query string to be properly urlencoded. - # See https://github.com/zappa/Zappa/issues/1227 for discussion of this behavior. - if "multiValueQueryStringParameters" in event_info: - query = event_info["multiValueQueryStringParameters"] - query_string = urlencode(query, doseq=True) if query else "" - else: + if event_version == "2.0": + # See the new format documentation + # here: https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + method = event_info["requestContext"]["http"]["method"] + headers = event_info["headers"] + if event_info.get("cookies"): + headers["cookie"] = "; ".join(event_info["cookies"]) + + path = urlencode(event_info["requestContext"]["http"]["path"]) + query = event_info.get("queryStringParameters", {}) query_string = urlencode(query) if query else "" + query_string = unquote(query_string) + + # Systems calling the Lambda (other than API Gateway) may not provide the field requestContext + # Extract remote_user, authorizer if Authorizer is enabled + remote_user = None + authorizer = event_info["requestContext"].get("authorizer", None) + if authorizer: + if authorizer.get("lambda"): + # Need to add principalId manually to lambda authorizer response context + remote_user = authorizer["lambda"].get("principalId") + elif authorizer.get("iam"): + remote_user = authorizer["iam"].get("userArn") + else: + method = event_info.get("httpMethod", None) + headers = merge_headers(event_info) or {} # Allow for the AGW console 'Test' button to work (Pull #735) + + path = unquote(event_info["path"]) + + # API Gateway and ALB both started allowing for multi-value querystring + # params in Nov. 2018. If there aren't multi-value params present, then + # it acts identically to 'queryStringParameters', so we can use it as a + # drop-in replacement. + # + # The one caveat here is that ALB will only include _one_ of + # queryStringParameters _or_ multiValueQueryStringParameters, which means + # we have to check for the existence of one and then fall back to the + # other. + + if "multiValueQueryStringParameters" in event_info: + query = event_info["multiValueQueryStringParameters"] + query_string = urlencode(query, doseq=True) if query else "" + else: + query = event_info.get("queryStringParameters", {}) + query_string = urlencode(query) if query else "" + query_string = urlencode(query_string) + + # Systems calling the Lambda (other than API Gateway) may not provide the field requestContext + # Extract remote_user, authorizer if Authorizer is enabled + remote_user = None + authorizer = None + if "requestContext" in event_info: + authorizer = event_info["requestContext"].get("authorizer", None) + if authorizer: + remote_user = authorizer.get("principalId") + elif event_info["requestContext"].get("identity"): + remote_user = event_info["requestContext"]["identity"].get("userArn") if context_header_mappings: for key, value in context_header_mappings.items(): @@ -70,12 +106,12 @@ def create_wsgi_request( encoded_body = event_info["body"] body = base64.b64decode(encoded_body) else: - body = event_info["body"] + body = event_info.get("body") if isinstance(body, str): body = body.encode("utf-8") else: - body = event_info["body"] + body = event_info.get("body") if isinstance(body, str): body = body.encode("utf-8") @@ -83,7 +119,6 @@ def create_wsgi_request( # https://github.com/Miserlou/Zappa/issues/1188 headers = titlecase_keys(headers) - path = unquote(event_info["path"]) if base_path: script_name = "/" + base_path @@ -122,16 +157,8 @@ def create_wsgi_request( "wsgi.run_once": False, } - # Systems calling the Lambda (other than API Gateway) may not provide the field requestContext - # Extract remote_user, authorizer if Authorizer is enabled - remote_user = None - if "requestContext" in event_info: - authorizer = event_info["requestContext"].get("authorizer", None) - if authorizer: - remote_user = authorizer.get("principalId") - environ["API_GATEWAY_AUTHORIZER"] = authorizer - elif event_info["requestContext"].get("identity"): - remote_user = event_info["requestContext"]["identity"].get("userArn") + if authorizer: + environ["API_GATEWAY_AUTHORIZER"] = authorizer # Input processing if method in ["POST", "PUT", "PATCH", "DELETE"]: From 4c5e4b8df8eb2f90fa66396f11824a98b7e162a3 Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 3 Oct 2023 15:06:52 +0800 Subject: [PATCH 056/111] Update wsgi.py --- zappa/wsgi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zappa/wsgi.py b/zappa/wsgi.py index 44356854f..d0d3546a7 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -32,7 +32,7 @@ def create_wsgi_request( if event_info.get("cookies"): headers["cookie"] = "; ".join(event_info["cookies"]) - path = urlencode(event_info["requestContext"]["http"]["path"]) + path = unquote(event_info["requestContext"]["http"]["path"]) query = event_info.get("queryStringParameters", {}) query_string = urlencode(query) if query else "" @@ -70,7 +70,7 @@ def create_wsgi_request( else: query = event_info.get("queryStringParameters", {}) query_string = urlencode(query) if query else "" - query_string = urlencode(query_string) + query_string = unquote(query_string) # Systems calling the Lambda (other than API Gateway) may not provide the field requestContext # Extract remote_user, authorizer if Authorizer is enabled From 2caf0d01e2fe2aff2a0d8cf2c43058a570f78e27 Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 12 Dec 2023 10:17:00 +0800 Subject: [PATCH 057/111] function url fixes --- zappa/core.py | 1 + zappa/handler.py | 4 ++++ zappa/wsgi.py | 3 +-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index d22fc4ed7..bf9928a25 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1662,6 +1662,7 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains "CachedMethods": {"Quantity": 3, "Items": ["HEAD", "GET", "OPTIONS"]}, }, "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", # no cache, details see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html + "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac", # noqa: E501 https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-except-host-header }, "Logging": {"Enabled": False, "IncludeCookies": False, "Bucket": "", "Prefix": ""}, "Restrictions": {"GeoRestriction": {"RestrictionType": "none", **NULL_CONFIG}}, diff --git a/zappa/handler.py b/zappa/handler.py index 32466217e..dac5a4aa9 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -531,6 +531,10 @@ def handler(self, event, context): # stage, so we must tell Flask to include the API # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 script_name = f"/{settings.API_STAGE}" + # fix function url domain + if host.find("lambda-url") > -1 and event.get("headers", {}).get("cloudfront-host"): + # https://stackoverflow.com/questions/73024633/cloudfront-forward-host-header-to-lambda-function-url-origin + event["headers"]["host"] = event["headers"]["cloudfront-host"] else: # This is a test request sent from the AWS console if settings.DOMAIN: diff --git a/zappa/wsgi.py b/zappa/wsgi.py index d0d3546a7..170f22966 100644 --- a/zappa/wsgi.py +++ b/zappa/wsgi.py @@ -32,8 +32,7 @@ def create_wsgi_request( if event_info.get("cookies"): headers["cookie"] = "; ".join(event_info["cookies"]) - path = unquote(event_info["requestContext"]["http"]["path"]) - + path = unquote(event_info["rawPath"]) query = event_info.get("queryStringParameters", {}) query_string = urlencode(query) if query else "" query_string = unquote(query_string) From 008640555b8cc32d1ee68e8954f0935640f5ddfd Mon Sep 17 00:00:00 2001 From: Sha Date: Tue, 12 Dec 2023 10:41:32 +0800 Subject: [PATCH 058/111] function url fix --- zappa/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index bf9928a25..bd25aa702 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1616,8 +1616,7 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains NULL_CONFIG = {"Quantity": 0, "Items": []} config = { - "CallerReference": "zappa-create-function-url-custom-domain", - "Aliases": {"Quantity": len(function_url_domains), "Items": function_url_domains}, + "CallerReference": "zappa-create-function-url-custom-domain-" + function_name.split(":")[-1], "Aliases": {"Quantity": len(function_url_domains), "Items": function_url_domains}, "DefaultRootObject": "", "Enabled": True, "PriceClass": "PriceClass_100", @@ -1696,7 +1695,9 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains id = distributions[0]["Id"] distribution = self.cloudfront_client.get_distribution(Id=id) new_config = distribution["Distribution"]["DistributionConfig"] - new_config.update(config) + updates = config.copy() + updates.pop("CallerReference") + new_config.update(updates) response = self.cloudfront_client.update_distribution( DistributionConfig=new_config, Id=id, IfMatch=distribution["ETag"] From 3b4f46af6b9afc481e833e88700ac9c136999b32 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 13 Dec 2023 14:50:29 +0800 Subject: [PATCH 059/111] added touch to function url --- zappa/cli.py | 27 ++++++++++++++------------- zappa/core.py | 5 +++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index 83ab8c695..617e1f0a3 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -858,7 +858,8 @@ def deploy(self, source_zip=None, docker_image_uri=None): function_name=self.lambda_arn, function_url_config=self.function_url_config, ) - self.zappa.deploy_lambda_function_url(**kwargs) + endpoint_url = self.zappa.deploy_lambda_function_url(**kwargs) + self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name) if self.use_apigateway: # Create and configure the API Gateway @@ -904,9 +905,8 @@ def deploy(self, source_zip=None, docker_image_uri=None): else: self.zappa.add_api_stage_to_api_key(api_key=self.api_key, api_id=api_id, stage_name=self.api_stage) - if self.stage_config.get("touch", True): - self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name) - self.touch_endpoint(endpoint_url) + if self.stage_config.get("touch", True): + self.touch_endpoint(endpoint_url) # Finally, delete the local copy our zip package if not source_zip and not docker_image_uri: @@ -1126,7 +1126,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): function_name=self.lambda_arn, function_url_config=self.function_url_config, ) - self.zappa.update_lambda_function_url(**kwargs) + endpoint_url = self.zappa.update_lambda_function_url(**kwargs)[:-1] else: self.zappa.delete_lambda_function_url(self.lambda_arn) @@ -1145,6 +1145,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): endpoint_url += "/" + self.base_path deployed_string = "Your updated Zappa deployment is " + click.style("live", fg="green", bold=True) + "!" + touch_url = endpoint_url if self.use_apigateway: deployed_string = deployed_string + ": " + click.style("{}".format(endpoint_url), bold=True) @@ -1154,13 +1155,11 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): if endpoint_url != api_url: deployed_string = deployed_string + " (" + api_url + ")" - - if self.stage_config.get("touch", True): - self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name) if api_url: - self.touch_endpoint(api_url) - elif endpoint_url: - self.touch_endpoint(endpoint_url) + touch_url = api_url + + if self.stage_config.get("touch", True): + self.touch_endpoint(touch_url) click.echo(deployed_string) @@ -1230,8 +1229,8 @@ def undeploy(self, no_confirm=False, remove_logs=False): if self.use_alb: self.zappa.undeploy_lambda_alb(self.lambda_name) - if self.function_url_domains: - self.zappa.undeploy_function_url_custom_domain(self.lambda_name) + # if self.function_url_domains: + # self.zappa.undeploy_function_url_custom_domain(self.lambda_name) if self.use_apigateway: if remove_logs: @@ -3047,6 +3046,8 @@ def touch_endpoint(self, endpoint_url): + " response code." ) + if req.status_code == 200: + click.echo(req.text) #################################################################### # Main diff --git a/zappa/core.py b/zappa/core.py index bd25aa702..041cd6c74 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1567,7 +1567,7 @@ def deploy_lambda_function_url(self, function_name, function_url_config): ) print("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(function_name, function_url_config) - return response + return response["FunctionUrl"] def update_lambda_function_url(self, function_name, function_url_config): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) @@ -1593,8 +1593,9 @@ def update_lambda_function_url(self, function_name, function_url_config): ) print("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(config["FunctionArn"], function_url_config) + return response["FunctionUrl"] else: - self.deploy_lambda_function_url(function_name, function_url_config) + return self.deploy_lambda_function_url(function_name, function_url_config) def delete_lambda_function_url(self, function_name): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) From 95f65a6a3458c5662500ab16832dbc7585f859c9 Mon Sep 17 00:00:00 2001 From: Sha Date: Thu, 1 Aug 2024 17:36:27 +0800 Subject: [PATCH 060/111] Update Pipfile --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index f8d05fbd9..866e50883 100644 --- a/Pipfile +++ b/Pipfile @@ -39,6 +39,7 @@ tqdm = "*" troposphere = ">=3.0" Werkzeug = "*" wheel = "*" +setuptools = {version = "*", extras = ["core"]} [pipenv] allow_prereleases = false From 10eab8eaef6ff74ae38b00ac909c56f5cf9e9424 Mon Sep 17 00:00:00 2001 From: Sha Date: Thu, 1 Aug 2024 17:39:40 +0800 Subject: [PATCH 061/111] Update Pipfile --- Pipfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Pipfile b/Pipfile index 866e50883..f8d05fbd9 100644 --- a/Pipfile +++ b/Pipfile @@ -39,7 +39,6 @@ tqdm = "*" troposphere = ">=3.0" Werkzeug = "*" wheel = "*" -setuptools = {version = "*", extras = ["core"]} [pipenv] allow_prereleases = false From 5ce68cbc42fd873f2463a84d8f64b606f98c4535 Mon Sep 17 00:00:00 2001 From: Sha Date: Thu, 28 Nov 2024 10:15:56 +0800 Subject: [PATCH 062/111] Update setup.py --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 5379cddac..2cac18753 100755 --- a/setup.py +++ b/setup.py @@ -46,11 +46,7 @@ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.2", From 266a3d7626bb4873f05beb68a724354b735f18ce Mon Sep 17 00:00:00 2001 From: Sha Date: Thu, 28 Nov 2024 10:57:20 +0800 Subject: [PATCH 063/111] Update __init__.py support 3.13 --- zappa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/__init__.py b/zappa/__init__.py index 3ef2445ef..e0f4258b3 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -12,7 +12,7 @@ def running_in_docker() -> bool: return running_in_docker_flag -SUPPORTED_VERSIONS = [(3, 8), (3, 9), (3, 10), (3, 11), (3, 12)] +SUPPORTED_VERSIONS = [(3, 8), (3, 9), (3, 10), (3, 11), (3, 12), (3, 13)] MINIMUM_SUPPORTED_MINOR_VERSION = 8 if not running_in_docker() and sys.version_info[:2] not in SUPPORTED_VERSIONS: From 9f65258f6415ec7a171e9aab55bd38ba34c2ca00 Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 6 Jan 2025 08:20:32 +0800 Subject: [PATCH 064/111] Update utilities.py add support for 3.13 --- zappa/utilities.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/zappa/utilities.py b/zappa/utilities.py index 132ff1fe5..1a603dae9 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -210,16 +210,8 @@ def get_runtime_from_python_version(): else: if sys.version_info[1] <= 7: raise ValueError("Python 3.7 and below are no longer supported.") - elif sys.version_info[1] == 8: - return "python3.8" - elif sys.version_info[1] == 9: - return "python3.9" - elif sys.version_info[1] == 10: - return "python3.10" - elif sys.version_info[1] == 11: - return "python3.11" - elif sys.version_info[1] == 12: - return "python3.12" + elif sys.version_info[1] in [8, 9, 10, 11, 12, 13]: + return "python3." + sys.version_info[1] else: raise ValueError(f"Python f{'.'.join(str(v) for v in sys.version_info[:2])} is not yet supported.") From fbb98fc76e480bc28e07c4a550a2655eaa37f0f2 Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 6 Jan 2025 08:31:27 +0800 Subject: [PATCH 065/111] pkg_resources was removed in python 3.13 --- zappa/cli.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index 8c43801d5..dd7dd651f 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -27,7 +27,7 @@ import botocore import click import hjson as json -import pkg_resources +# import pkg_resources import requests import slugify import toml @@ -194,11 +194,14 @@ def handle(self, argv=None): desc = "Zappa - Deploy Python applications to AWS Lambda" " and API Gateway.\n" parser = argparse.ArgumentParser(description=desc) + from importlib.metadata import version + + zappa_version = version("zappa") parser.add_argument( "-v", "--version", action="version", - version=pkg_resources.get_distribution("zappa").version, + version=zappa_version, help="Print the zappa version", ) parser.add_argument("--color", default="auto", choices=["auto", "never", "always"]) @@ -2214,7 +2217,9 @@ def check_for_update(self): Print a warning if there's a new Zappa version available. """ try: - version = pkg_resources.require("zappa")[0].version + from importlib.metadata import version + + version = version("zappa") updateable = check_new_version_available(version) if updateable: click.echo( From 3040cb66dec980258756fc0924bfc544f3f823aa Mon Sep 17 00:00:00 2001 From: Sha Date: Mon, 6 Jan 2025 08:33:44 +0800 Subject: [PATCH 066/111] Update utilities.py --- zappa/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/utilities.py b/zappa/utilities.py index 1a603dae9..3e98deeb3 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -211,7 +211,7 @@ def get_runtime_from_python_version(): if sys.version_info[1] <= 7: raise ValueError("Python 3.7 and below are no longer supported.") elif sys.version_info[1] in [8, 9, 10, 11, 12, 13]: - return "python3." + sys.version_info[1] + return "python3." + str(sys.version_info[1]) else: raise ValueError(f"Python f{'.'.join(str(v) for v in sys.version_info[:2])} is not yet supported.") From 0fcd07ede118c6a38e4d0359299a5eea753549c4 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 22 Jan 2025 11:14:09 +0800 Subject: [PATCH 067/111] added more debugging information --- zappa/cli.py | 8 +++++++- zappa/core.py | 12 +++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index dd7dd651f..a986256b0 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1279,6 +1279,13 @@ def schedule(self): Given a a list of functions and a schedule to execute them, setup up regular execution. """ + self.zappa.unschedule_events( + lambda_name=self.lambda_name, + lambda_arn=self.lambda_arn, + events=events, + excluded_source_services=["dynamodb", "kinesis", "sqs"], + ) + events = self.stage_config.get("events", []) if events: @@ -1302,7 +1309,6 @@ def schedule(self): "description": "Zappa Keep Warm - {}".format(self.lambda_name), } ) - if events: try: function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name) diff --git a/zappa/core.py b/zappa/core.py index 761e52dc0..3b10a3088 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2555,6 +2555,7 @@ def update_stack( try: self.cf_client.describe_stacks(StackName=name) except botocore.client.ClientError: + update = False if update_only and not update: @@ -2658,7 +2659,9 @@ def get_api_id(self, lambda_name): try: response = self.cf_client.describe_stack_resource(StackName=lambda_name, LogicalResourceId="Api") return response["StackResourceDetail"].get("PhysicalResourceId", None) - except Exception: # pragma: no cover + except Exception as e: # pragma: no cover + print(lambda_name) + print(e) try: # Try the old method (project was probably made on an older, non CF version) response = self.apigateway_client.get_rest_apis(limit=500) @@ -2668,6 +2671,7 @@ def get_api_id(self, lambda_name): return item["id"] logger.exception("Could not get API ID.") + logger.exception(response) return None except Exception: # pragma: no cover # We don't even have an API deployed. That's okay! @@ -3080,12 +3084,6 @@ def schedule_events(self, lambda_arn, lambda_name, events, default=True): # if default: # lambda_arn = lambda_arn + ":$LATEST" - self.unschedule_events( - lambda_name=lambda_name, - lambda_arn=lambda_arn, - events=events, - excluded_source_services=pull_services, - ) for event in events: function = event["function"] expression = event.get("expression", None) # single expression From b3d2007ec7f6bdfe06b01dee38e63d41ab823e3e Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 22 Jan 2025 11:25:48 +0800 Subject: [PATCH 068/111] revert changes --- zappa/cli.py | 2 +- zappa/core.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index a986256b0..7adb7dc10 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1282,7 +1282,7 @@ def schedule(self): self.zappa.unschedule_events( lambda_name=self.lambda_name, lambda_arn=self.lambda_arn, - events=events, + events=[], excluded_source_services=["dynamodb", "kinesis", "sqs"], ) diff --git a/zappa/core.py b/zappa/core.py index 3b10a3088..51f3a2a97 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -3078,7 +3078,12 @@ def schedule_events(self, lambda_arn, lambda_name, events, default=True): # and do not require event permissions. They do require additional permissions on the Lambda roles though. # http://docs.aws.amazon.com/lambda/latest/dg/lambda-api-permissions-ref.html pull_services = ["dynamodb", "kinesis", "sqs"] - + self.unschedule_events( + lambda_name=lambda_name, + lambda_arn=lambda_arn, + events=events, + excluded_source_services=pull_services, + ) # XXX: Not available in Lambda yet. # We probably want to execute the latest code. # if default: From 34ab8da7b5491853c1c0f62984661583c2775221 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 22 Jan 2025 11:41:10 +0800 Subject: [PATCH 069/111] Update core.py --- zappa/core.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 51f3a2a97..3ebdc614a 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2660,8 +2660,7 @@ def get_api_id(self, lambda_name): response = self.cf_client.describe_stack_resource(StackName=lambda_name, LogicalResourceId="Api") return response["StackResourceDetail"].get("PhysicalResourceId", None) except Exception as e: # pragma: no cover - print(lambda_name) - print(e) + logger.exception(e) try: # Try the old method (project was probably made on an older, non CF version) response = self.apigateway_client.get_rest_apis(limit=500) @@ -2670,7 +2669,7 @@ def get_api_id(self, lambda_name): if item["name"] == lambda_name: return item["id"] - logger.exception("Could not get API ID.") + logger.exception(f"Could not get API ID. {lambda_name}") logger.exception(response) return None except Exception: # pragma: no cover From 6a63fb4bd8f154b3703bdc26267b37eb75537af2 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 22 Jan 2025 12:08:07 +0800 Subject: [PATCH 070/111] Update core.py --- zappa/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 3ebdc614a..207e1d419 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2555,7 +2555,6 @@ def update_stack( try: self.cf_client.describe_stacks(StackName=name) except botocore.client.ClientError: - update = False if update_only and not update: @@ -2669,7 +2668,7 @@ def get_api_id(self, lambda_name): if item["name"] == lambda_name: return item["id"] - logger.exception(f"Could not get API ID. {lambda_name}") + logger.exception(f"Could not get API ID. {lambda_name} {self.boto_session.region_name}") logger.exception(response) return None except Exception: # pragma: no cover From 2f2de0c1a46487d0c389c7770368457c57a0f74e Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 22 Jan 2025 12:48:45 +0800 Subject: [PATCH 071/111] Update cli.py --- zappa/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/cli.py b/zappa/cli.py index 7adb7dc10..77e5edd58 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -545,7 +545,7 @@ def dispatch_command(self, command, stage): + click.style(self.api_stage, bold=True) + ".." ) - + click.echo(self.vargs) # Explicitly define the app function. # Related: https://github.com/Miserlou/Zappa/issues/832 if self.vargs.get("app_function", None): From 263b14d57308d1fd3853c2fb4eebfb8300534ac2 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 22 Jan 2025 13:00:36 +0800 Subject: [PATCH 072/111] Update cli.py --- zappa/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zappa/cli.py b/zappa/cli.py index 77e5edd58..a2dc338fc 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -929,7 +929,8 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): """ Repackage and update the function code. """ - + click.echo(self.stage_config) + click.echo(self.zappa.aws_region) if not source_zip and not docker_image_uri: # Make sure we're in a venv. self.check_venv() From dd4145c8ce63828c2879267a526fb8475a590896 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 22 Jan 2025 13:06:22 +0800 Subject: [PATCH 073/111] Update core.py enforce explicit region in settings file --- zappa/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 207e1d419..dea9bd589 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -3595,11 +3595,10 @@ def load_credentials(self, boto_session=None, profile_name=None): if profile_name: self.boto_session = boto3.Session(profile_name=profile_name, region_name=self.aws_region) elif os.environ.get("AWS_ACCESS_KEY_ID") and os.environ.get("AWS_SECRET_ACCESS_KEY"): - region_name = os.environ.get("AWS_DEFAULT_REGION") or self.aws_region session_kw = { "aws_access_key_id": os.environ.get("AWS_ACCESS_KEY_ID"), "aws_secret_access_key": os.environ.get("AWS_SECRET_ACCESS_KEY"), - "region_name": region_name, + "region_name": self.aws_region, } # If we're executing in a role, AWS_SESSION_TOKEN will be present, too. From 622ea8abfbade247189e377b405fdf51a83218a6 Mon Sep 17 00:00:00 2001 From: Sha Date: Thu, 23 Jan 2025 15:09:00 +0800 Subject: [PATCH 074/111] Update Pipfile --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 24a31acc2..6c4929df2 100644 --- a/Pipfile +++ b/Pipfile @@ -40,6 +40,7 @@ tqdm = "*" troposphere = ">=3.0" Werkzeug = "*" wheel = "*" +setuptools = {version = "*", extras = ["core"]} [pipenv] allow_prereleases = false From a0b718aca38751b2671d85d90b4ea72be486037b Mon Sep 17 00:00:00 2001 From: Sha Date: Thu, 23 Jan 2025 15:09:52 +0800 Subject: [PATCH 075/111] Update Pipfile --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 6c4929df2..f5226da94 100644 --- a/Pipfile +++ b/Pipfile @@ -40,7 +40,7 @@ tqdm = "*" troposphere = ">=3.0" Werkzeug = "*" wheel = "*" -setuptools = {version = "*", extras = ["core"]} +setuptools = "*" [pipenv] allow_prereleases = false From fe95b27c146c11df670a4578053fe0d6b211e1db Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 26 Mar 2025 16:17:55 +0800 Subject: [PATCH 076/111] snapstart support --- tests/test_settings.yaml | 1 + tests/tests.py | 42 ++++++++++++++++++++++++++++++++++++++++ zappa/cli.py | 3 +++ zappa/core.py | 4 ++++ 4 files changed, 50 insertions(+) diff --git a/tests/test_settings.yaml b/tests/test_settings.yaml index bd0136fde..78a0d3d60 100644 --- a/tests/test_settings.yaml +++ b/tests/test_settings.yaml @@ -55,3 +55,4 @@ extendofail: environment_variables: EXTENDO: You bet + snap_start: None diff --git a/tests/tests.py b/tests/tests.py index fb286922d..390933932 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -674,6 +674,46 @@ def test_update_layers(self): z.update_lambda_configuration("test", "test", "test") self.assertEqual(mock_client.update_function_configuration.call_args[1]["Layers"], []) + def test_snap_start_configuration(self): + """ + Test that SnapStart configuration is correctly set in Lambda configuration. + """ + # Test with SnapStart explicitly enabled + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "snap_start_enabled" + zappa_cli.load_settings("test_settings.yaml") + self.assertEqual("PublishedVersions", zappa_cli.snap_start) + + # Test with SnapStart explicitly disabled + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "snap_start_disabled" + zappa_cli.load_settings("test_settings.yaml") + self.assertEqual("None", zappa_cli.snap_start) + + # Test that SnapStart is properly passed to boto3 + with mock.patch.object(Zappa, 'create_lambda_function') as mock_create_lambda: + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "snap_start_enabled" + zappa_cli.load_settings("test_settings.yaml") + zappa_cli.zappa = Zappa() + zappa_cli.deploy("test.zip", None) + + # Check that the SnapStart setting was correctly passed + create_args = mock_create_lambda.call_args[1] + self.assertEqual("PublishedVersions", create_args['snap_start']) + + # Test that SnapStart is properly passed to Lambda update + with mock.patch.object(Zappa, 'update_lambda_configuration') as mock_update_lambda: + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "snap_start_enabled" + zappa_cli.load_settings("test_settings.json") + zappa_cli.zappa = Zappa() + zappa_cli.update(None, True, None) + + # Check that the SnapStart setting was correctly passed + update_args = mock_update_lambda.call_args[1] + self.assertEqual("PublishedVersions", update_args['snap_start']) + def test_update_empty_aws_env_hash(self): z = Zappa() z.credentials_arn = object() @@ -2635,5 +2675,7 @@ def test_wsgi_query_string_with_encodechars(self): self.assertEqual(request["QUERY_STRING"], expected) + + if __name__ == "__main__": unittest.main() diff --git a/zappa/cli.py b/zappa/cli.py index 5cd4147b4..45b12c97a 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -119,6 +119,7 @@ class ZappaCLI: authorizer = None xray_tracing = False aws_kms_key_arn = "" + snap_start = None context_header_mappings = None additional_text_mimetypes = None tags = [] # type: ignore[var-annotated] @@ -1062,6 +1063,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): aws_environment_variables=self.aws_environment_variables, aws_kms_key_arn=self.aws_kms_key_arn, layers=self.layers, + snap_start=self.snap_start, wait=False, ) @@ -2299,6 +2301,7 @@ def load_settings(self, settings_file=None, session=None): self.authorizer = self.stage_config.get("authorizer", {}) self.runtime = self.stage_config.get("runtime", get_runtime_from_python_version()) self.aws_kms_key_arn = self.stage_config.get("aws_kms_key_arn", "") + self.snap_start = self.snap_start.get("snap_start", "None") self.context_header_mappings = self.stage_config.get("context_header_mappings", {}) self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") diff --git a/zappa/core.py b/zappa/core.py index 12a143be0..3c73f34e7 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1085,6 +1085,7 @@ def create_lambda_function( runtime="python3.8", aws_environment_variables=None, aws_kms_key_arn=None, + snap_start=None, xray_tracing=False, local_zip=None, use_alb=False, @@ -1122,6 +1123,7 @@ def create_lambda_function( Environment={"Variables": aws_environment_variables}, KMSKeyArn=aws_kms_key_arn, TracingConfig={"Mode": "Active" if self.xray_tracing else "PassThrough"}, + SnapStart={'ApplyOn': snap_start if snap_start else 'None'}, Layers=layers, ) if not docker_image_uri: @@ -1271,6 +1273,7 @@ def update_lambda_configuration( aws_environment_variables=None, aws_kms_key_arn=None, layers=None, + snap_start=None, wait=True, ): """ @@ -1314,6 +1317,7 @@ def update_lambda_configuration( "Environment": {"Variables": aws_environment_variables}, "KMSKeyArn": aws_kms_key_arn, "TracingConfig": {"Mode": "Active" if self.xray_tracing else "PassThrough"}, + "SnapStart": {'ApplyOn': snap_start if snap_start else "None"} } if lambda_aws_config.get("PackageType", None) != "Image": From e0e44745d628f7df74e0ad683d301cbd3101ac59 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 26 Mar 2025 16:20:51 +0800 Subject: [PATCH 077/111] Update test_settings.yaml --- tests/test_settings.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_settings.yaml b/tests/test_settings.yaml index 78a0d3d60..618341168 100644 --- a/tests/test_settings.yaml +++ b/tests/test_settings.yaml @@ -54,5 +54,9 @@ extendofail: prebuild_script: test_settings.prebuild_me environment_variables: EXTENDO: You bet - +snap_start_enabled: + extends: ttt888 + snap_start: PublishedVersions +snap_start_disabled: + extends: ttt888 snap_start: None From 79ed00a4df8ed74804a14345d008724964face8f Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 26 Mar 2025 16:28:09 +0800 Subject: [PATCH 078/111] Update core.py --- zappa/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/core.py b/zappa/core.py index 3c73f34e7..2554aa237 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -481,7 +481,7 @@ def create_handler_venv(self, use_zappa_release: Optional[str] = None): # This is the recommended method for installing packages if you don't # to depend on `setuptools` # https://github.com/pypa/pip/issues/5240#issuecomment-381662679 - pip_process = subprocess.Popen(command, stdout=subprocess.PIPE) + pip_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Using communicate() to avoid deadlocks stdout_result, stderror_result = pip_process.communicate() pip_return_code = pip_process.returncode From 50431b41823e4db6a130c5dfb39400684569fe36 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 26 Mar 2025 16:33:12 +0800 Subject: [PATCH 079/111] lint fix --- tests/tests.py | 10 ++++------ zappa/core.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 390933932..f300d585b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -691,7 +691,7 @@ def test_snap_start_configuration(self): self.assertEqual("None", zappa_cli.snap_start) # Test that SnapStart is properly passed to boto3 - with mock.patch.object(Zappa, 'create_lambda_function') as mock_create_lambda: + with mock.patch.object(Zappa, "create_lambda_function") as mock_create_lambda: zappa_cli = ZappaCLI() zappa_cli.api_stage = "snap_start_enabled" zappa_cli.load_settings("test_settings.yaml") @@ -700,10 +700,10 @@ def test_snap_start_configuration(self): # Check that the SnapStart setting was correctly passed create_args = mock_create_lambda.call_args[1] - self.assertEqual("PublishedVersions", create_args['snap_start']) + self.assertEqual("PublishedVersions", create_args["snap_start"]) # Test that SnapStart is properly passed to Lambda update - with mock.patch.object(Zappa, 'update_lambda_configuration') as mock_update_lambda: + with mock.patch.object(Zappa, "update_lambda_configuration") as mock_update_lambda: zappa_cli = ZappaCLI() zappa_cli.api_stage = "snap_start_enabled" zappa_cli.load_settings("test_settings.json") @@ -712,7 +712,7 @@ def test_snap_start_configuration(self): # Check that the SnapStart setting was correctly passed update_args = mock_update_lambda.call_args[1] - self.assertEqual("PublishedVersions", update_args['snap_start']) + self.assertEqual("PublishedVersions", update_args["snap_start"]) def test_update_empty_aws_env_hash(self): z = Zappa() @@ -2675,7 +2675,5 @@ def test_wsgi_query_string_with_encodechars(self): self.assertEqual(request["QUERY_STRING"], expected) - - if __name__ == "__main__": unittest.main() diff --git a/zappa/core.py b/zappa/core.py index 2554aa237..80164fba9 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1123,7 +1123,7 @@ def create_lambda_function( Environment={"Variables": aws_environment_variables}, KMSKeyArn=aws_kms_key_arn, TracingConfig={"Mode": "Active" if self.xray_tracing else "PassThrough"}, - SnapStart={'ApplyOn': snap_start if snap_start else 'None'}, + SnapStart={"ApplyOn": snap_start if snap_start else "None"}, Layers=layers, ) if not docker_image_uri: @@ -1317,7 +1317,7 @@ def update_lambda_configuration( "Environment": {"Variables": aws_environment_variables}, "KMSKeyArn": aws_kms_key_arn, "TracingConfig": {"Mode": "Active" if self.xray_tracing else "PassThrough"}, - "SnapStart": {'ApplyOn': snap_start if snap_start else "None"} + "SnapStart": {"ApplyOn": snap_start if snap_start else "None"}, } if lambda_aws_config.get("PackageType", None) != "Image": From de237e3e361cc024f1533077a846bea47aea9b78 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 26 Mar 2025 16:38:30 +0800 Subject: [PATCH 080/111] enhance documentation --- README.md | 292 +++++++++++++++++++++++++++------------------------ zappa/cli.py | 2 +- 2 files changed, 153 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index de52ee815..ce3bfe6cc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ - - [About](#about) - [Installation and Configuration](#installation-and-configuration) - [Running the Initial Setup / Settings](#running-the-initial-setup--settings) @@ -48,7 +47,7 @@ - [Running Tasks in a VPC](#running-tasks-in-a-vpc) - [Responses](#responses) - [Advanced Settings](#advanced-settings) - - [YAML Settings](#yaml-settings) + - [YAML Settings](#yaml-settings) - [Advanced Usage](#advanced-usage) - [Keeping The Server Warm](#keeping-the-server-warm) - [Serving Static Files / Binary Uploads](#serving-static-files--binary-uploads) @@ -86,7 +85,7 @@ - [Related Projects](#related-projects) - [Hacks](#hacks) - [Contributing](#contributing) - - [Using a Local Repo](#using-a-local-repo) + - [Using a Local Repo](#using-a-local-repo) @@ -125,7 +124,7 @@ Zappa also lets you build hybrid event-driven applications that can scale to **t And finally, Zappa is **super easy to use**. You can deploy your application with a single command out of the box! -__Awesome!__ +**Awesome!**

Zappa Demo Gif @@ -151,7 +150,7 @@ Next, you'll need to define your local and server-side settings. $ zappa init -This will automatically detect your application type (Flask/Django - Pyramid users [see here](https://github.com/Miserlou/Zappa/issues/278#issuecomment-241917956)) and help you define your deployment configuration settings. Once you finish initialization, you'll have a file named *zappa_settings.json* in your project directory defining your basic deployment settings. It will probably look something like this for most WSGI apps: +This will automatically detect your application type (Flask/Django - Pyramid users [see here](https://github.com/Miserlou/Zappa/issues/278#issuecomment-241917956)) and help you define your deployment configuration settings. Once you finish initialization, you'll have a file named _zappa_settings.json_ in your project directory defining your basic deployment settings. It will probably look something like this for most WSGI apps: ```javascript { @@ -203,7 +202,7 @@ And now your app is **live!** How cool is that?! To explain what's going on, when you call `deploy`, Zappa will automatically package up your application and local virtual environment into a Lambda-compatible archive, replace any dependencies with versions with wheels compatible with lambda, set up the function handler and necessary WSGI Middleware, upload the archive to S3, create and manage the necessary Amazon IAM policies and roles, register it as a new Lambda function, create a new API Gateway resource, create WSGI-compatible routes for it, link it to the new Lambda function, and finally delete the archive from your S3 bucket. Handy! Be aware that the default IAM role and policy created for executing Lambda applies a liberal set of permissions. -These are most likely not appropriate for production deployment of important applications. See the section +These are most likely not appropriate for production deployment of important applications. See the section [Custom AWS IAM Roles and Policies for Execution](#custom-aws-iam-roles-and-policies-for-execution) for more detail. ### Updates @@ -218,10 +217,10 @@ This creates a new archive, uploads it to S3 and updates the Lambda function to #### Docker Workflows -In [version 0.53.0](https://github.com/zappa/Zappa/blob/master/CHANGELOG.md), support was added to deploy & update Lambda functions using Docker. +In [version 0.53.0](https://github.com/zappa/Zappa/blob/master/CHANGELOG.md), support was added to deploy & update Lambda functions using Docker. You can specify an ECR image using the `--docker-image-uri` option to the zappa command on `deploy` and `update`. -Zappa expects that the image is built and pushed to a Amazon ECR repository. +Zappa expects that the image is built and pushed to a Amazon ECR repository. Deploy Example: @@ -253,7 +252,7 @@ You can also `rollback` the deployed code to a previous version by supplying the Zappa can be used to easily schedule functions to occur on regular intervals. This provides a much nicer, maintenance-free alternative to Celery! These functions will be packaged and deployed along with your `app_function` and called from the handler automatically. -Just list your functions and the expression to schedule them using [cron or rate syntax](http://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html) in your *zappa_settings.json* file: +Just list your functions and the expression to schedule them using [cron or rate syntax](http://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html) in your _zappa_settings.json_ file: ```javascript { @@ -286,7 +285,7 @@ See the [example](example/) for more details. ##### Multiple Expressions -Sometimes a function needs multiple expressions to describe its schedule. To set multiple expressions, simply list your functions, and the list of expressions to schedule them using [cron or rate syntax](http://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html) in your *zappa_settings.json* file: +Sometimes a function needs multiple expressions to describe its schedule. To set multiple expressions, simply list your functions, and the list of expressions to schedule them using [cron or rate syntax](http://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html) in your _zappa_settings.json_ file: ```javascript { @@ -368,10 +367,10 @@ Zappa will automatically package your active virtual environment into a package During this process, it will replace any local dependencies with AWS Lambda compatible versions. Dependencies are included in this order: - * Lambda-compatible `manylinux` wheels from a local cache - * Lambda-compatible `manylinux` wheels from PyPI - * Packages from the active virtual environment - * Packages from the local project directory +- Lambda-compatible `manylinux` wheels from a local cache +- Lambda-compatible `manylinux` wheels from PyPI +- Packages from the active virtual environment +- Packages from the local project directory It also skips certain unnecessary files, and ignores any .py files if .pyc files are available. @@ -379,8 +378,8 @@ In addition, Zappa will also automatically set the correct execution permissions To further reduce the final package file size, you can: -* Set `slim_handler` to `True` to upload a small handler to Lambda and the rest of the package to S3. For more details, see the [merged pull request](https://github.com/Miserlou/Zappa/pull/548) and the [discussion in the original issue](https://github.com/Miserlou/Zappa/issues/510). See also: [Large Projects](#large-projects). -* Use the `exclude` or `exclude_glob` setting and provide a list of patterns to exclude from the archive. By default, Zappa will exclude Boto, because [it's already available in the Lambda execution environment](http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html). +- Set `slim_handler` to `True` to upload a small handler to Lambda and the rest of the package to S3. For more details, see the [merged pull request](https://github.com/Miserlou/Zappa/pull/548) and the [discussion in the original issue](https://github.com/Miserlou/Zappa/issues/510). See also: [Large Projects](#large-projects). +- Use the `exclude` or `exclude_glob` setting and provide a list of patterns to exclude from the archive. By default, Zappa will exclude Boto, because [it's already available in the Lambda execution environment](http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html). ### Template @@ -432,7 +431,6 @@ To tail logs without following (to exit immediately after displaying the end of $ zappa tail production --since 1h --disable-keep-open - ### Remote Function Invocation You can execute any function in your application directly at any time by using the `invoke` command. @@ -516,7 +514,7 @@ However, it's now far easier to use Route 53-based DNS authentication, which wil Similarly, you can have your functions execute in response to events that happen in the AWS ecosystem, such as S3 uploads, DynamoDB entries, Kinesis streams, SNS messages, and SQS queues. -In your *zappa_settings.json* file, define your [event sources](http://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html) and the function you wish to execute. For instance, this will execute `your_module.process_upload_function` in response to new objects in your `my-bucket` S3 bucket. Note that `process_upload_function` must accept `event` and `context` parameters. +In your _zappa_settings.json_ file, define your [event sources](http://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html) and the function you wish to execute. For instance, this will execute `your_module.process_upload_function` in response to new objects in your `my-bucket` S3 bucket. Note that `process_upload_function` must accept `event` and `context` parameters. ```javascript { @@ -620,7 +618,7 @@ Optionally you can add [SNS message filters](http://docs.aws.amazon.com/sns/late ] ``` -[SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) is also pulling messages from a stream. At this time, [only "Standard" queues can trigger lambda events, not "FIFO" queues](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html). Read the AWS Documentation carefully since Lambda calls the SQS DeleteMessage API on your behalf once your function completes successfully. +[SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) is also pulling messages from a stream. At this time, [only "Standard" queues can trigger lambda events, not "FIFO" queues](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html). Read the AWS Documentation carefully since Lambda calls the SQS DeleteMessage API on your behalf once your function completes successfully. ```javascript "events": [ @@ -636,6 +634,7 @@ Optionally you can add [SNS message filters](http://docs.aws.amazon.com/sns/late ``` For configuring Lex Bot's intent triggered events: + ```javascript "bot_events": [ { @@ -651,6 +650,7 @@ For configuring Lex Bot's intent triggered events: ``` Events can also take keyword arguments: + ```javascript "events": [ { @@ -669,7 +669,6 @@ def your_recurring_function(event, context): ``` - You can find more [example event sources here](http://docs.aws.amazon.com/lambda/latest/dg/eventsources.html). ## Asynchronous Task Execution @@ -705,6 +704,7 @@ the functions will execute immediately and locally. The zappa asynchronous funct when in the Lambda environment or when specifying [Remote Invocations](https://github.com/zappa/zappa#remote-invocations). ### Catching Exceptions + Putting a try..except block on an asynchronous task like this: ```python @@ -773,30 +773,31 @@ run(your_function, args, kwargs, service='sns') # Using SNS ### Remote Invocations By default, Zappa will use lambda's current function name and current AWS region. If you wish to invoke a lambda with - a different function name/region or invoke your lambda from outside of lambda, you must specify the - `remote_aws_lambda_function_name` and `remote_aws_region` arguments so that the application knows which function and - region to use. For example, if some part of our pizza making application had to live on an EC2 instance, but we - wished to call the make_pie() function on its own Lambda instance, we would do it as follows: +a different function name/region or invoke your lambda from outside of lambda, you must specify the +`remote_aws_lambda_function_name` and `remote_aws_region` arguments so that the application knows which function and +region to use. For example, if some part of our pizza making application had to live on an EC2 instance, but we +wished to call the make_pie() function on its own Lambda instance, we would do it as follows: - ```python +```python @task(remote_aws_lambda_function_name='pizza-pie-prod', remote_aws_region='us-east-1') def make_pie(): - """ This takes a long time! """ - ingredients = get_ingredients() - pie = bake(ingredients) - deliver(pie) + """ This takes a long time! """ + ingredients = get_ingredients() + pie = bake(ingredients) + deliver(pie) ``` + If those task() parameters were not used, then EC2 would execute the function locally. These same - `remote_aws_lambda_function_name` and `remote_aws_region` arguments can be used on the zappa.asynchronous.run() function as well. +`remote_aws_lambda_function_name` and `remote_aws_region` arguments can be used on the zappa.asynchronous.run() function as well. ### Restrictions The following restrictions to this feature apply: -* Functions must have a clean import path -- i.e. no closures, lambdas, or methods. -* `args` and `kwargs` must be JSON-serializable. -* The JSON-serialized arguments must be within the size limits for Lambda (256K) or SNS (256K) events. +- Functions must have a clean import path -- i.e. no closures, lambdas, or methods. +- `args` and `kwargs` must be JSON-serializable. +- The JSON-serialized arguments must be within the size limits for Lambda (256K) or SNS (256K) events. All of this code is still backwards-compatible with non-Lambda environments - it simply executes in a blocking fashion and returns the result. @@ -804,16 +805,18 @@ All of this code is still backwards-compatible with non-Lambda environments - it If you're running Zappa in a Virtual Private Cloud (VPC), you'll need to configure your subnets to allow your lambda to communicate with services inside your VPC as well as the public Internet. A minimal setup requires two subnets. -In __subnet-a__: -* Create a NAT -* Create an Internet gateway -* In the route table, create a route pointing the Internet gateway to 0.0.0.0/0. +In **subnet-a**: -In __subnet-b__: -* Place your lambda function -* In the route table, create a route pointing the NAT that belongs to __subnet-a__ to 0.0.0.0/0. +- Create a NAT +- Create an Internet gateway +- In the route table, create a route pointing the Internet gateway to 0.0.0.0/0. -You can place your lambda in multiple subnets that are configured the same way as __subnet-b__ for high availability. +In **subnet-b**: + +- Place your lambda function +- In the route table, create a route pointing the NAT that belongs to **subnet-a** to 0.0.0.0/0. + +You can place your lambda in multiple subnets that are configured the same way as **subnet-b** for high availability. Some helpful resources are [this tutorial](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Tutorials.WebServerDB.CreateVPC.html), [this other tutorial](https://gist.github.com/reggi/dc5f2620b7b4f515e68e46255ac042a7) and [this AWS doc page](http://docs.aws.amazon.com/lambda/latest/dg/vpc.html#vpc-internet). @@ -989,6 +992,7 @@ to change Zappa's behavior. Use these at your own risk! "runtime": "python3.12", // Python runtime to use on Lambda. Can be one of: "python3.8", "python3.9", "python3.10", "python3.11", or "python3.12". Defaults to whatever the current Python being used is. "s3_bucket": "dev-bucket", // Zappa zip bucket, "slim_handler": false, // Useful if project >50M. Set true to just upload a small handler to Lambda and load actual project from S3 at runtime. Default false. + "snap_start": "PublishedVersions", // Enable Lambda SnapStart for faster cold starts. Can be "PublishedVersions" or "None". Default "None". "settings_file": "~/Projects/MyApp/settings/dev_settings.py", // Server side settings file location, "tags": { // Attach additional tags to AWS Resources "Key": "Value", // Example Key and value @@ -1017,11 +1021,11 @@ dev: app_function: your_module.your_app s3_bucket: your-code-bucket events: - - function: your_module.your_function - event_source: - arn: arn:aws:s3:::your-event-bucket - events: - - s3:ObjectCreated:* + - function: your_module.your_function + event_source: + arn: arn:aws:s3:::your-event-bucket + events: + - s3:ObjectCreated:* ``` You can also supply a custom settings file at any time with the `-s` argument, ex: @@ -1075,7 +1079,7 @@ Bash completion can be enabled by adding the following to your .bashrc: `register-python-argcomplete` is provided by the argcomplete Python package. If this package was installed in a virtualenv then the command must be run there. Alternatively you can execute: - activate-global-python-argcomplete --dest=- > file +activate-global-python-argcomplete --dest=- > file The file's contents should then be sourced in e.g. ~/.bashrc. @@ -1084,10 +1088,11 @@ The file's contents should then be sourced in e.g. ~/.bashrc. #### API Key You can use the `api_key_required` setting to generate an API key to all the routes of your API Gateway. The process is as follows: -1. Deploy/redeploy (update won't work) and write down the *id* for the key that has been created + +1. Deploy/redeploy (update won't work) and write down the _id_ for the key that has been created 2. Go to AWS console > Amazon API Gateway and - * select "API Keys" and find the key *value* (for example `key_value`) - * select "Usage Plans", create a new usage plan and link the API Key and the API that Zappa has created for you + - select "API Keys" and find the key _value_ (for example `key_value`) + - select "Usage Plans", create a new usage plan and link the API Key and the API that Zappa has created for you 3. Send a request where you pass the key value as a header called `x-api-key` to access the restricted endpoints (for example with curl: `curl --header "x-api-key: key_value"`). Note that without the x-api-key header, you will receive a 403. #### IAM Policy @@ -1095,6 +1100,7 @@ You can use the `api_key_required` setting to generate an API key to all the rou You can enable IAM-based (v4 signing) authorization on an API by setting the `iam_authorization` setting to `true`. Your API will then require signed requests and access can be controlled via [IAM policy](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-iam-policy-examples.html). Unsigned requests will receive a 403 response, as will requesters who are not authorized to access the API. Enabling this will override the Authorizer configuration (see below). #### API Gateway Lambda Authorizers + If you deploy an API endpoint with Zappa, you can take advantage of [API Gateway Lambda Authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) to implement a token-based authentication - all you need to do is to provide a function to create the required output, Zappa takes care of the rest. A good start for the function is the [AWS Labs blueprint example](https://github.com/awslabs/aws-apigateway-lambda-authorizer-blueprints/blob/master/blueprints/python/api-gateway-authorizer-python.py). If you are wondering for what you would use an Authorizer, here are some potential use cases: @@ -1108,8 +1114,8 @@ Zappa can be configured to call a function inside your code to do the authorizat For example, to get the Cognito identity, add this to a `zappa_settings.yaml`: ```yaml - context_header_mappings: - user_id: authorizer.user_id +context_header_mappings: + user_id: authorizer.user_id ``` Which can now be accessed in Flask like this: @@ -1193,7 +1199,6 @@ If your project needs to be aware of the type of environment you're deployed to, If you want to use native AWS Lambda environment variables you can use the `aws_environment_variables` configuration setting. These are useful as you can easily change them via the AWS Lambda console or cli at runtime. They are also useful for storing sensitive credentials and to take advantage of KMS encryption of environment variables. - During development, you can add your Zappa defined variables to your locally running app by, for example, using the below (for Django, to manage.py). ```python @@ -1216,12 +1221,12 @@ _Note: if you rely on these as well as `environment_variables`, and you have the _S3 remote environment variables were added to Zappa before AWS introduced native environment variables for Lambda (via the console and cli). Before going down this route check if above make more sense for your usecase._ - If you want to use remote environment variables to configure your application (which is especially useful for things like sensitive credentials), you can create a file and place it in an S3 bucket to which your Zappa application has access. To do this, add the `remote_env` key to zappa_settings pointing to a file containing a flat JSON object, so that each key-value pair on the object will be set as an environment variable and value whenever a new lambda instance spins up. For example, to ensure your application has access to the database credentials without storing them in your version control, you can add a file to S3 with the connection string and load it into the lambda environment using the `remote_env` configuration setting. super-secret-config.json (uploaded to my-config-bucket): + ```javascript { "DB_CONNECTION_STRING": "super-secret:database" @@ -1229,6 +1234,7 @@ super-secret-config.json (uploaded to my-config-bucket): ``` zappa_settings.json: + ```javascript { "dev": { @@ -1240,6 +1246,7 @@ zappa_settings.json: ``` Now in your application you can use: + ```python import os db_string = os.environ.get('DB_CONNECTION_STRING') @@ -1281,6 +1288,7 @@ For example, if you want to expose the $context.identity.cognitoIdentityId varia By default, if an _unhandled_ exception happens in your code, Zappa will just print the stacktrace into a CloudWatch log. If you wish to use an external reporting tool to take note of those exceptions, you can use the `exception_handler` configuration option. zappa_settings.json: + ```javascript { "dev": { @@ -1294,11 +1302,13 @@ zappa_settings.json: The function has to accept three arguments: exception, event, and context: your_module.py + ```python def unhandled_exceptions(e, event, context): send_to_raygun(e, event) # gather data you need and send return True # Prevent invocation retry ``` + You may still need a similar exception handler inside your application, this is just a way to catch exception which happen at the Zappa/WSGI layer (typically event-based invocations, misconfigured settings, bad Lambda packages, and permissions issues). By default, AWS Lambda will attempt to retry an event based (non-API Gateway, e.g. CloudWatch) invocation if an exception has been thrown. However, you can prevent this by returning True, as in example above, so Zappa that will not re-raise the uncaught exception, thus preventing AWS Lambda from retrying the current invocation. @@ -1320,8 +1330,8 @@ resources. While this allows most Lambdas to work correctly with no extra permis generally not an acceptable set of permissions for most continuous integration pipelines or production deployments. Instead, you will probably want to manually manage your IAM policies. -To manually define the policy of your Lambda execution role, you must set *manage_roles* to false and define -either the *role_name* or *role_arn* in your Zappa settings file. +To manually define the policy of your Lambda execution role, you must set _manage_roles_ to false and define +either the _role_name_ or _role_arn_ in your Zappa settings file. ```javascript { @@ -1391,7 +1401,6 @@ Note that you may create subsegments in your code but an exception will be raise ### Globally Available Server-less Architectures -

Global Zappa Slides

@@ -1417,7 +1426,7 @@ You must have already created the corresponding SNS/SQS topic/queue, and the Lam ### Unique Package ID -For monitoring of different deployments, a unique UUID for each package is available in `package_info.json` in the root directory of your application's package. You can use this information or a hash of this file for such things as tracking errors across different deployments, monitoring status of deployments and other such things on services such as Sentry and New Relic. The package will contain: +For monitoring of different deployments, a unique UUID for each package is available in `package_info.json` in the root directory of your application's package. You can use this information or a hash of this file for such things as tracking errors across different deployments, monitoring status of deployments and other such things on services such as Sentry and New Relic. The package will contain: ```json { @@ -1431,11 +1440,13 @@ For monitoring of different deployments, a unique UUID for each package is avail ### Application Load Balancer Event Source Zappa can be used to handle events triggered by Application Load Balancers (ALB). This can be useful in a few circumstances: + - Since API Gateway has a hard limit of 30 seconds before timing out, you can use an ALB for longer running requests. - API Gateway is billed per-request; therefore, costs can become excessive with high throughput services. ALBs pricing model makes much more sense financially if you're expecting a lot of traffic to your Lambda. - ALBs can be placed within a VPC, which may make more sense for private endpoints than using API Gateway's private model (using AWS PrivateLink). -Like API Gateway, Zappa can automatically provision ALB resources for you. You'll need to add the following to your `zappa_settings`: +Like API Gateway, Zappa can automatically provision ALB resources for you. You'll need to add the following to your `zappa_settings`: + ``` "alb_enabled": true, "alb_vpc_config": { @@ -1451,7 +1462,7 @@ Like API Gateway, Zappa can automatically provision ALB resources for you. You' More information on using ALB as an event source for Lambda can be found [here](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html). -*An important note*: right now, Zappa will provision ONE lambda to ONE load balancer, which means using `base_path` along with ALB configuration is currently unsupported. +_An important note_: right now, Zappa will provision ONE lambda to ONE load balancer, which means using `base_path` along with ALB configuration is currently unsupported. ### Endpoint Configuration @@ -1462,6 +1473,7 @@ For full list of options for endpoint configuration refer to [API Gateway Endpoi #### Example Private API Gateway configuration zappa_settings.json: + ```json { "dev": { @@ -1475,28 +1487,29 @@ zappa_settings.json: ``` apigateway_resource_policy.json: + ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Deny", - "Principal": "*", - "Action": "execute-api:Invoke", - "Resource": "execute-api:/*", - "Condition": { - "StringNotEquals": { - "aws:sourceVpc": "{{vpcID}}" // UPDATE ME - } - } - }, - { - "Effect": "Allow", - "Principal": "*", - "Action": "execute-api:Invoke", - "Resource": "execute-api:/*" + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": "*", + "Action": "execute-api:Invoke", + "Resource": "execute-api:/*", + "Condition": { + "StringNotEquals": { + "aws:sourceVpc": "{{vpcID}}" // UPDATE ME } - ] + } + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "execute-api:Invoke", + "Resource": "execute-api:/*" + } + ] } ``` @@ -1506,82 +1519,81 @@ Lambda may provide additional resources than provisioned during cold start initi ## Zappa Guides -* [Django-Zappa tutorial (screencast)](https://www.youtube.com/watch?v=plUrbPN0xc8&feature=youtu.be). -* [Using Django-Zappa, Part 1](https://serverlesscode.com/post/zappa-wsgi-for-python/). -* [Using Django-Zappa, Part 2: VPCs](https://serverlesscode.com/post/zappa-wsgi-for-python-pt-2/). -* [Building Serverless Microservices with Zappa and Flask](https://gun.io/blog/serverless-microservices-with-zappa-and-flask/) -* [Zappa で Hello World するまで (Japanese)](http://qiita.com/satoshi_iwashita/items/505492193317819772c7) -* [How to Deploy Zappa with CloudFront, RDS and VPC](https://jinwright.net/how-deploy-serverless-wsgi-app-using-zappa/) -* [Secure 'Serverless' File Uploads with AWS Lambda, S3, and Zappa](http://blog.stratospark.com/secure-serverless-file-uploads-with-aws-lambda-s3-zappa.html) -* [Deploy a Serverless WSGI App using Zappa, CloudFront, RDS, and VPC](https://docs.google.com/presentation/d/1aYeOMgQl4V_fFgT5VNoycdXtob1v6xVUWlyxoTEiTw0/edit#slide=id.p) -* [AWS: Deploy Alexa Ask Skills with Flask-Ask and Zappa](https://developer.amazon.com/blogs/post/8e8ad73a-99e9-4c0f-a7b3-60f92287b0bf/New-Alexa-Tutorial-Deploy-Flask-Ask-Skills-to-AWS-Lambda-with-Zappa) -* [Guide to using Django with Zappa](https://edgarroman.github.io/zappa-django-guide/) -* [Zappa and LambCI](https://seancoates.com/blogs/zappa-and-lambci/) -* [Building A Serverless Image Processing SaaS using Zappa](https://medium.com/99serverless/building-a-serverless-image-processing-saas-9ef68b594076) -* [Serverless Slack Slash Commands with Python and Zappa](https://renzo.lucioni.xyz/serverless-slash-commands-with-python/) -* [Bringing Tokusatsu to AWS using Python, Flask, Zappa and Contentful](https://www.contentful.com/blog/2018/03/07/bringing-tokusatsu-to-aws-using-python-flask-zappa-and-contentful/) -* [AWS Summit 2018 Seoul - Zappa와 함께하는 Serverless Microservice](https://www.slideshare.net/YunSeopSong/zappa-serverless-microservice-94410308/) -* [Book - Building Serverless Python Web Services with Zappa](https://github.com/PacktPublishing/Building-Serverless-Python-Web-Services-with-Zappa) -* [Vider sa flask dans une lambda](http://free_zed.gitlab.io/articles/2019/11/vider-sa-flask-dans-une-lambda/)[French] -* _Your guide here?_ +- [Django-Zappa tutorial (screencast)](https://www.youtube.com/watch?v=plUrbPN0xc8&feature=youtu.be). +- [Using Django-Zappa, Part 1](https://serverlesscode.com/post/zappa-wsgi-for-python/). +- [Using Django-Zappa, Part 2: VPCs](https://serverlesscode.com/post/zappa-wsgi-for-python-pt-2/). +- [Building Serverless Microservices with Zappa and Flask](https://gun.io/blog/serverless-microservices-with-zappa-and-flask/) +- [Zappa で Hello World するまで (Japanese)](http://qiita.com/satoshi_iwashita/items/505492193317819772c7) +- [How to Deploy Zappa with CloudFront, RDS and VPC](https://jinwright.net/how-deploy-serverless-wsgi-app-using-zappa/) +- [Secure 'Serverless' File Uploads with AWS Lambda, S3, and Zappa](http://blog.stratospark.com/secure-serverless-file-uploads-with-aws-lambda-s3-zappa.html) +- [Deploy a Serverless WSGI App using Zappa, CloudFront, RDS, and VPC](https://docs.google.com/presentation/d/1aYeOMgQl4V_fFgT5VNoycdXtob1v6xVUWlyxoTEiTw0/edit#slide=id.p) +- [AWS: Deploy Alexa Ask Skills with Flask-Ask and Zappa](https://developer.amazon.com/blogs/post/8e8ad73a-99e9-4c0f-a7b3-60f92287b0bf/New-Alexa-Tutorial-Deploy-Flask-Ask-Skills-to-AWS-Lambda-with-Zappa) +- [Guide to using Django with Zappa](https://edgarroman.github.io/zappa-django-guide/) +- [Zappa and LambCI](https://seancoates.com/blogs/zappa-and-lambci/) +- [Building A Serverless Image Processing SaaS using Zappa](https://medium.com/99serverless/building-a-serverless-image-processing-saas-9ef68b594076) +- [Serverless Slack Slash Commands with Python and Zappa](https://renzo.lucioni.xyz/serverless-slash-commands-with-python/) +- [Bringing Tokusatsu to AWS using Python, Flask, Zappa and Contentful](https://www.contentful.com/blog/2018/03/07/bringing-tokusatsu-to-aws-using-python-flask-zappa-and-contentful/) +- [AWS Summit 2018 Seoul - Zappa와 함께하는 Serverless Microservice](https://www.slideshare.net/YunSeopSong/zappa-serverless-microservice-94410308/) +- [Book - Building Serverless Python Web Services with Zappa](https://github.com/PacktPublishing/Building-Serverless-Python-Web-Services-with-Zappa) +- [Vider sa flask dans une lambda](http://free_zed.gitlab.io/articles/2019/11/vider-sa-flask-dans-une-lambda/)[French] +- _Your guide here?_ ## Zappa in the Press -* _[Zappa Serves Python, Minus the Servers](http://www.infoworld.com/article/3031665/application-development/zappa-serves-python-web-apps-minus-the-servers.html)_ -* _[Zappa lyfter serverlösa applikationer med Python](http://computersweden.idg.se/2.2683/1.649895/zappa-lyfter-python)_ -* _[Interview: Rich Jones on Zappa](https://serverlesscode.com/post/rich-jones-interview-django-zappa/)_ -* [Top 10 Python Libraries of 2016](https://tryolabs.com/blog/2016/12/20/top-10-python-libraries-of-2016/) +- _[Zappa Serves Python, Minus the Servers](http://www.infoworld.com/article/3031665/application-development/zappa-serves-python-web-apps-minus-the-servers.html)_ +- _[Zappa lyfter serverlösa applikationer med Python](http://computersweden.idg.se/2.2683/1.649895/zappa-lyfter-python)_ +- _[Interview: Rich Jones on Zappa](https://serverlesscode.com/post/rich-jones-interview-django-zappa/)_ +- [Top 10 Python Libraries of 2016](https://tryolabs.com/blog/2016/12/20/top-10-python-libraries-of-2016/) ## Sites Using Zappa -* [Mailchimp Signup Utility](https://github.com/sasha42/Mailchimp-utility) - A microservice for adding people to a mailing list via API. -* [Zappa Slack Inviter](https://github.com/Miserlou/zappa-slack-inviter) - A tiny, server-less service for inviting new users to your Slack channel. -* [Serverless Image Host](https://github.com/Miserlou/serverless-imagehost) - A thumbnailing service with Flask, Zappa and Pillow. -* [Zappa BitTorrent Tracker](https://github.com/Miserlou/zappa-bittorrent-tracker) - An experimental server-less BitTorrent tracker. Work in progress. -* [JankyGlance](https://github.com/Miserlou/JankyGlance) - A server-less Yahoo! Pipes replacement. -* [LambdaMailer](https://github.com/tryolabs/lambda-mailer) - A server-less endpoint for processing a contact form. -* [Voter Registration Microservice](https://topics.arlingtonva.us/2016/11/voter-registration-search-microservice/) - Official backup to to the Virginia Department of Elections portal. -* [FreePoll Online](https://www.freepoll.online) - A simple and awesome say for groups to make decisions. -* [PasteOfCode](https://paste.ofcode.org/) - A Zappa-powered paste bin. -* And many more, including banks, governments, startups, enterprises and schools! +- [Mailchimp Signup Utility](https://github.com/sasha42/Mailchimp-utility) - A microservice for adding people to a mailing list via API. +- [Zappa Slack Inviter](https://github.com/Miserlou/zappa-slack-inviter) - A tiny, server-less service for inviting new users to your Slack channel. +- [Serverless Image Host](https://github.com/Miserlou/serverless-imagehost) - A thumbnailing service with Flask, Zappa and Pillow. +- [Zappa BitTorrent Tracker](https://github.com/Miserlou/zappa-bittorrent-tracker) - An experimental server-less BitTorrent tracker. Work in progress. +- [JankyGlance](https://github.com/Miserlou/JankyGlance) - A server-less Yahoo! Pipes replacement. +- [LambdaMailer](https://github.com/tryolabs/lambda-mailer) - A server-less endpoint for processing a contact form. +- [Voter Registration Microservice](https://topics.arlingtonva.us/2016/11/voter-registration-search-microservice/) - Official backup to to the Virginia Department of Elections portal. +- [FreePoll Online](https://www.freepoll.online) - A simple and awesome say for groups to make decisions. +- [PasteOfCode](https://paste.ofcode.org/) - A Zappa-powered paste bin. +- And many more, including banks, governments, startups, enterprises and schools! Are you using Zappa? Let us know and we'll list your site here! ## Related Projects -* [Mackenzie](http://github.com/Miserlou/Mackenzie) - AWS Lambda Infection Toolkit -* [NoDB](https://github.com/Miserlou/NoDB) - A simple, server-less, Pythonic object store based on S3. -* [zappa-cms](http://github.com/Miserlou/zappa-cms) - A tiny server-less CMS for busy hackers. Work in progress. -* [zappa-django-utils](https://github.com/Miserlou/zappa-django-utils) - Utility commands to help Django deployments. -* [flask-ask](https://github.com/johnwheeler/flask-ask) - A framework for building Amazon Alexa applications. Uses Zappa for deployments. -* [zappa-file-widget](https://github.com/anush0247/zappa-file-widget) - A Django plugin for supporting binary file uploads in Django on Zappa. -* [zops](https://github.com/bjinwright/zops) - Utilities for teams and continuous integrations using Zappa. -* [cookiecutter-mobile-backend](https://github.com/narfman0/cookiecutter-mobile-backend/) - A `cookiecutter` Django project with Zappa and S3 uploads support. -* [zappa-examples](https://github.com/narfman0/zappa-examples/) - Flask, Django, image uploads, and more! -* [zappa-hug-example](https://github.com/mcrowson/zappa-hug-example) - Example of a Hug application using Zappa. -* [Zappa Docker Image](https://github.com/danielwhatmuff/zappa) - A Docker image for running Zappa locally, based on Lambda Docker. -* [zappa-dashing](https://github.com/nikos/zappa-dashing) - Monitor your AWS environment (health/metrics) with Zappa and CloudWatch. -* [s3env](https://github.com/cameronmaske/s3env) - Manipulate a remote Zappa environment variable key/value JSON object file in an S3 bucket through the CLI. -* [zappa_resize_image_on_fly](https://github.com/wobeng/zappa_resize_image_on_fly) - Resize images on the fly using Flask, Zappa, Pillow, and OpenCV-python. -* [zappa-ffmpeg](https://github.com/ubergarm/zappa-ffmpeg) - Run ffmpeg inside a lambda for serverless transformations. -* [gdrive-lambda](https://github.com/richiverse/gdrive-lambda) - pass json data to a csv file for end users who use Gdrive across the organization. -* [travis-build-repeat](https://github.com/bcongdon/travis-build-repeat) - Repeat TravisCI builds to avoid stale test results. -* [wunderskill-alexa-skill](https://github.com/mcrowson/wunderlist-alexa-skill) - An Alexa skill for adding to a Wunderlist. -* [xrayvision](https://github.com/mathom/xrayvision) - Utilities and wrappers for using AWS X-Ray with Zappa. -* [terraform-aws-zappa](https://github.com/dpetzold/terraform-aws-zappa) - Terraform modules for creating a VPC, RDS instance, ElastiCache Redis and CloudFront Distribution for use with Zappa. -* [zappa-sentry](https://github.com/jneves/zappa-sentry) - Integration with Zappa and Sentry -* [IOpipe](https://github.com/iopipe/iopipe-python#zappa) - Monitor, profile and analyze your Zappa apps. - +- [Mackenzie](http://github.com/Miserlou/Mackenzie) - AWS Lambda Infection Toolkit +- [NoDB](https://github.com/Miserlou/NoDB) - A simple, server-less, Pythonic object store based on S3. +- [zappa-cms](http://github.com/Miserlou/zappa-cms) - A tiny server-less CMS for busy hackers. Work in progress. +- [zappa-django-utils](https://github.com/Miserlou/zappa-django-utils) - Utility commands to help Django deployments. +- [flask-ask](https://github.com/johnwheeler/flask-ask) - A framework for building Amazon Alexa applications. Uses Zappa for deployments. +- [zappa-file-widget](https://github.com/anush0247/zappa-file-widget) - A Django plugin for supporting binary file uploads in Django on Zappa. +- [zops](https://github.com/bjinwright/zops) - Utilities for teams and continuous integrations using Zappa. +- [cookiecutter-mobile-backend](https://github.com/narfman0/cookiecutter-mobile-backend/) - A `cookiecutter` Django project with Zappa and S3 uploads support. +- [zappa-examples](https://github.com/narfman0/zappa-examples/) - Flask, Django, image uploads, and more! +- [zappa-hug-example](https://github.com/mcrowson/zappa-hug-example) - Example of a Hug application using Zappa. +- [Zappa Docker Image](https://github.com/danielwhatmuff/zappa) - A Docker image for running Zappa locally, based on Lambda Docker. +- [zappa-dashing](https://github.com/nikos/zappa-dashing) - Monitor your AWS environment (health/metrics) with Zappa and CloudWatch. +- [s3env](https://github.com/cameronmaske/s3env) - Manipulate a remote Zappa environment variable key/value JSON object file in an S3 bucket through the CLI. +- [zappa_resize_image_on_fly](https://github.com/wobeng/zappa_resize_image_on_fly) - Resize images on the fly using Flask, Zappa, Pillow, and OpenCV-python. +- [zappa-ffmpeg](https://github.com/ubergarm/zappa-ffmpeg) - Run ffmpeg inside a lambda for serverless transformations. +- [gdrive-lambda](https://github.com/richiverse/gdrive-lambda) - pass json data to a csv file for end users who use Gdrive across the organization. +- [travis-build-repeat](https://github.com/bcongdon/travis-build-repeat) - Repeat TravisCI builds to avoid stale test results. +- [wunderskill-alexa-skill](https://github.com/mcrowson/wunderlist-alexa-skill) - An Alexa skill for adding to a Wunderlist. +- [xrayvision](https://github.com/mathom/xrayvision) - Utilities and wrappers for using AWS X-Ray with Zappa. +- [terraform-aws-zappa](https://github.com/dpetzold/terraform-aws-zappa) - Terraform modules for creating a VPC, RDS instance, ElastiCache Redis and CloudFront Distribution for use with Zappa. +- [zappa-sentry](https://github.com/jneves/zappa-sentry) - Integration with Zappa and Sentry +- [IOpipe](https://github.com/iopipe/iopipe-python#zappa) - Monitor, profile and analyze your Zappa apps. ## Hacks Zappa goes quite far beyond what Lambda and API Gateway were ever intended to handle. As a result, there are quite a few hacks in here that allow it to work. Some of those include, but aren't limited to.. -* Using VTL to map body, headers, method, params and query strings into JSON, and then turning that into valid WSGI. -* Attaching response codes to response bodies, Base64 encoding the whole thing, using that as a regex to route the response code, decoding the body in VTL, and mapping the response body to that. -* Packing and _Base58_ encoding multiple cookies into a single cookie because we can only map one kind. -* Forcing the case permutations of "Set-Cookie" in order to return multiple headers at the same time. -* Turning cookie-setting 301/302 responses into 200 responses with HTML redirects, because we have no way to set headers on redirects. +- Using VTL to map body, headers, method, params and query strings into JSON, and then turning that into valid WSGI. +- Attaching response codes to response bodies, Base64 encoding the whole thing, using that as a regex to route the response code, decoding the body in VTL, and mapping the response body to that. +- Packing and _Base58_ encoding multiple cookies into a single cookie because we can only map one kind. +- Forcing the case permutations of "Set-Cookie" in order to return multiple headers at the same time. +- Turning cookie-setting 301/302 responses into 200 responses with HTML redirects, because we have no way to set headers on redirects. ## Contributing @@ -1600,4 +1612,4 @@ Zappa does not intend to conform to PEP8, isolate your commits so that changes t #### Using a Local Repo -To use the git HEAD, you *probably can't* use `pip install -e `. Instead, you should clone the repo to your machine and then `pip install /path/to/zappa/repo` or `ln -s /path/to/zappa/repo/zappa zappa` in your local project. +To use the git HEAD, you _probably can't_ use `pip install -e `. Instead, you should clone the repo to your machine and then `pip install /path/to/zappa/repo` or `ln -s /path/to/zappa/repo/zappa zappa` in your local project. diff --git a/zappa/cli.py b/zappa/cli.py index 45b12c97a..fa3ce2738 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -2301,7 +2301,7 @@ def load_settings(self, settings_file=None, session=None): self.authorizer = self.stage_config.get("authorizer", {}) self.runtime = self.stage_config.get("runtime", get_runtime_from_python_version()) self.aws_kms_key_arn = self.stage_config.get("aws_kms_key_arn", "") - self.snap_start = self.snap_start.get("snap_start", "None") + self.snap_start = self.stage_config.get("snap_start", "None") self.context_header_mappings = self.stage_config.get("context_header_mappings", {}) self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") From 42831ad7353672acd608ebed8d625de437d99c84 Mon Sep 17 00:00:00 2001 From: sha Date: Sun, 19 Oct 2025 11:30:57 +0800 Subject: [PATCH 081/111] added python 3.14 support --- README.md | 6 +- example/authmodule.py | 6 +- setup.py | 1 + tests/test_core.py | 80 +++++++++++++++++++++---- tests/test_handler.py | 5 +- tests/test_utilities.py | 5 +- tests/test_wsgi_binary_support_app.py | 2 - tests/test_wsgi_script_name_settings.py | 2 +- zappa/__init__.py | 10 +++- zappa/cli.py | 15 ++++- zappa/core.py | 78 ++++++++++++++++-------- zappa/utilities.py | 31 ++++++++-- 12 files changed, 183 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index f4c1d0b7c..f89a4b141 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ And finally, Zappa is **super easy to use**. You can deploy your application wit ## Installation and Configuration -_Before you begin, make sure you are running Python 3.8/3.9/3.10/3.11/3.12/3.13 and you have a valid AWS account and your [AWS credentials file](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) is properly installed._ +_Before you begin, make sure you are running Python 3.8/3.9/3.10/3.11/3.12/3.13/3.14 and you have a valid AWS account and your [AWS credentials file](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) is properly installed._ **Zappa** can easily be installed through pip, like so: @@ -447,7 +447,7 @@ For instance, suppose you have a basic application in a file called "my_app.py", Any remote print statements made and the value the function returned will then be printed to your local console. **Nifty!** -You can also invoke interpretable Python 3.8/3.9/3.10/3.11/3.12/3.13 strings directly by using `--raw`, like so: +You can also invoke interpretable Python 3.8/3.9/3.10/3.11/3.12/3.13/3.14 strings directly by using `--raw`, like so: $ zappa invoke production "print(1 + 2 + 3)" --raw @@ -1008,7 +1008,7 @@ to change Zappa's behavior. Use these at your own risk! "role_name": "MyLambdaRole", // Name of Zappa execution role. Default --ZappaExecutionRole. To use a different, pre-existing policy, you must also set manage_roles to false. "role_arn": "arn:aws:iam::12345:role/app-ZappaLambdaExecutionRole", // ARN of Zappa execution role. Default to None. To use a different, pre-existing policy, you must also set manage_roles to false. This overrides role_name. Use with temporary credentials via GetFederationToken. "route53_enabled": true, // Have Zappa update your Route53 Hosted Zones when certifying with a custom domain. Default true. - "runtime": "python3.13", // Python runtime to use on Lambda. Can be one of: "python3.8", "python3.9", "python3.10", "python3.11", "python3.12", or "python3.13". Defaults to whatever the current Python being used is. + "runtime": "python3.14", // Python runtime to use on Lambda. Can be one of: "python3.8", "python3.9", "python3.10", "python3.11", "python3.12", "python3.13", or "python3.14". Defaults to whatever the current Python being used is. "s3_bucket": "dev-bucket", // Zappa zip bucket, "slim_handler": false, // Useful if project >50M. Set true to just upload a small handler to Lambda and load actual project from S3 at runtime. Default false. "snap_start": "PublishedVersions", // Enable Lambda SnapStart for faster cold starts. Can be "PublishedVersions" or "None". Default "None". diff --git a/example/authmodule.py b/example/authmodule.py index a11b07e90..8f936182e 100644 --- a/example/authmodule.py +++ b/example/authmodule.py @@ -188,13 +188,15 @@ def denyMethod(self, verb, resource): def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy - conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" + conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition + """ self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy - conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" + conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition + """ self._addMethod("Deny", verb, resource, conditions) def build(self): diff --git a/setup.py b/setup.py index bc4f35beb..4ef4ea25e 100755 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.2", diff --git a/tests/test_core.py b/tests/test_core.py index fede6b81a..f3bc4e71c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -224,6 +224,14 @@ def test_get_manylinux_python313(self): self.assertTrue(os.path.isfile(path)) os.remove(path) + def test_manylinux_pattern_python314(self): + z = Zappa(runtime="python3.14") + wheel_filename = "psycopg_binary-3.2.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + abi3_wheel_filename = "cryptography-44.0.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + + self.assertTrue(z.manylinux_wheel_file_match.match(wheel_filename)) + self.assertTrue(z.manylinux_wheel_file_match.match(abi3_wheel_filename)) + # same, but with an ABI3 package mock_installed_packages = {"cryptography": "44.0.2"} with mock.patch( @@ -276,7 +284,8 @@ def test_verify_manylinux_filename_is_lowered(self): self.assertEqual(file_name, expected_filename) mock_get.assert_called_once_with( - "https://pypi.python.org/pypi/markupsafe/json", timeout=float(os.environ.get("PIP_TIMEOUT", 1.5)) + "https://pypi.python.org/pypi/markupsafe/json", + timeout=float(os.environ.get("PIP_TIMEOUT", 1.5)), ) # Clean the generated files @@ -298,7 +307,10 @@ def test_get_exclude_glob__file_not_deleted(self): return_value=mock_installed_packages, ): z = Zappa(runtime="python3.11") - path = z.create_lambda_zip(handler_file=os.path.realpath(__file__), exclude_glob=[str(file_to_not_delete)]) + path = z.create_lambda_zip( + handler_file=os.path.realpath(__file__), + exclude_glob=[str(file_to_not_delete)], + ) self.assertTrue(os.path.isfile(path)) self.assertTrue(file_to_not_delete.exists()) os.remove(file_to_not_delete) @@ -318,7 +330,10 @@ def test_getting_installed_packages(self, *args): mock_pip_installed_packages = [sample_package] with mock.patch("importlib.metadata.distributions", return_value=mock_pip_installed_packages): - self.assertDictEqual(z.get_installed_packages(site_packages=site_packages), {"super_package": "0.1"}) + self.assertDictEqual( + z.get_installed_packages(site_packages=site_packages), + {"super_package": "0.1"}, + ) def test_get_current_venv(self, *args): z = Zappa() @@ -344,7 +359,11 @@ def test_getting_installed_packages_mixed_case_location(self, *args): z = Zappa(runtime="python3.8") mock_pip_installed_packages = [] - for package_name, version, location in ("SuperPackage", "0.1", "/Venv/site-packages"), ( + for package_name, version, location in ( + "SuperPackage", + "0.1", + "/Venv/site-packages", + ), ( "SuperPackage64", "0.1", "/Venv/site-packages64", @@ -388,7 +407,10 @@ def test_getting_installed_packages_mixed_case(self, *args): mock_pip_installed_packages = [sample_package] with mock.patch("importlib.metadata.distributions", return_value=mock_pip_installed_packages): - self.assertDictEqual(z.get_installed_packages(site_packages=site_packages), {"superpackage": "0.1"}) + self.assertDictEqual( + z.get_installed_packages(site_packages=site_packages), + {"superpackage": "0.1"}, + ) def test_load_credentials(self): z = Zappa() @@ -1252,7 +1274,14 @@ def test_wsgi_from_v2_event(self): def test_wsgi_from_v2_event_with_lambda_authorizer(self): principal_id = "user|a1b2c3d4" - authorizer = {"lambda": {"bool": True, "key": "value", "number": 1, "principalId": principal_id}} + authorizer = { + "lambda": { + "bool": True, + "key": "value", + "number": 1, + "principalId": principal_id, + } + } event = { "version": "2.0", "routeKey": "ANY /{proxy+}", @@ -1382,11 +1411,17 @@ def test_zappa_init(self): zappa_cli = ZappaCLI() with mock.patch("zappa.cli.ZappaCLI._get_init_env", return_value="dev"), mock.patch( - "zappa.cli.ZappaCLI._get_init_profile", return_value=("default", {"region": "us-west-2"}) - ), mock.patch("zappa.cli.ZappaCLI._get_init_bucket", return_value="my-zappa-bucket"), mock.patch( - "zappa.cli.ZappaCLI._get_init_django_settings", return_value="test_settings" + "zappa.cli.ZappaCLI._get_init_profile", + return_value=("default", {"region": "us-west-2"}), + ), mock.patch( + "zappa.cli.ZappaCLI._get_init_bucket", + return_value="my-zappa-bucket", + ), mock.patch( + "zappa.cli.ZappaCLI._get_init_django_settings", + return_value="test_settings", ), mock.patch( - "zappa.cli.ZappaCLI._get_init_global_settings", return_value=["n", False] + "zappa.cli.ZappaCLI._get_init_global_settings", + return_value=["n", False], ), mock.patch( "zappa.cli.ZappaCLI._get_init_confirm", return_value="y" ): @@ -1400,7 +1435,8 @@ def test_zappa_init(self): self.assertEqual(zappa_settings["dev"]["s3_bucket"], "my-zappa-bucket") self.assertEqual(zappa_settings["dev"]["django_settings"], "test_settings") self.assertEqual( - zappa_settings["dev"]["exclude"], ["boto3", "dateutil", "botocore", "s3transfer", "concurrent"] + zappa_settings["dev"]["exclude"], + ["boto3", "dateutil", "botocore", "s3transfer", "concurrent"], ) # delete the file @@ -1534,7 +1570,10 @@ def test_load_settings_additional_text_mimetypes(self): zappa_cli.api_stage = "addtextmimetypes" zappa_cli.load_settings("test_settings.json") expected_additional_text_mimetypes = ["application/custommimetype"] - self.assertEqual(expected_additional_text_mimetypes, zappa_cli.stage_config["additional_text_mimetypes"]) + self.assertEqual( + expected_additional_text_mimetypes, + zappa_cli.stage_config["additional_text_mimetypes"], + ) self.assertEqual(True, zappa_cli.stage_config["binary_support"]) def test_settings_extension(self): @@ -3051,6 +3090,17 @@ def test_unsupported_version_error(self, *_): reload(zappa) + @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 14)) + def test_supported_python_version(self, *_): + from importlib import reload + + try: + import zappa + + reload(zappa) + except RuntimeError as exc: # pragma: no cover + self.fail(f"RuntimeError raised for supported Python version: {exc}") + @mock.patch("os.getenv", return_value="True") @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 6)) def test_minor_version_only_check_when_in_docker(self, *_): @@ -3109,7 +3159,11 @@ def test_wsgi_query_string_with_encodechars(self): "pathParameters": {}, "path": "/path/path1", "httpMethod": "GET", - "queryStringParameters": {"query": "Jane&John", "otherquery": "B", "test": "hello+m.te&how&are&you"}, + "queryStringParameters": { + "query": "Jane&John", + "otherquery": "B", + "test": "hello+m.te&how&are&you", + }, "requestContext": {}, } request = create_wsgi_request(event) diff --git a/tests/test_handler.py b/tests/test_handler.py index 822c52b02..6c4f73a7b 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -599,7 +599,10 @@ def test_wsgi_script_name_on_v2_formatted_event(self): }, "isBase64Encoded": False, "body": "", - "cookies": ["Cookie_1=Value1; Expires=21 Oct 2021 07:48 GMT", "Cookie_2=Value2; Max-Age=78000"], + "cookies": [ + "Cookie_1=Value1; Expires=21 Oct 2021 07:48 GMT", + "Cookie_2=Value2; Max-Age=78000", + ], } response = lh.handler(event, None) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index e6652f8c2..0f0bd2556 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -44,7 +44,10 @@ def tearDown(self): # Give the user their AWS region back, we're done testing with us-east-1. os.environ["AWS_DEFAULT_REGION"] = self.users_current_region_name - @mock.patch("zappa.core.find_packages", return_value=["package", "package.subpackage", "package.another"]) + @mock.patch( + "zappa.core.find_packages", + return_value=["package", "package.subpackage", "package.another"], + ) def test_copy_editable_packages(self, mock_find_packages): virtual_env = Path(os.environ.get("VIRTUAL_ENV")) if not virtual_env: diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py index 747aa695f..b4d9bfb50 100644 --- a/tests/test_wsgi_binary_support_app.py +++ b/tests/test_wsgi_binary_support_app.py @@ -6,10 +6,8 @@ import gzip import json - from flask import Flask, Response - app = Flask(__name__) diff --git a/tests/test_wsgi_script_name_settings.py b/tests/test_wsgi_script_name_settings.py index 750810a14..4174cf73f 100644 --- a/tests/test_wsgi_script_name_settings.py +++ b/tests/test_wsgi_script_name_settings.py @@ -10,4 +10,4 @@ LOG_LEVEL = "DEBUG" PROJECT_NAME = "wsgi_script_name_settings" COGNITO_TRIGGER_MAPPING = {} -EXCEPTION_HANDLER = None \ No newline at end of file +EXCEPTION_HANDLER = None diff --git a/zappa/__init__.py b/zappa/__init__.py index 160b4c445..737ef1c20 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -8,11 +8,17 @@ def running_in_docker() -> bool: - When docker is used allow usage of any python version """ # https://stackoverflow.com/questions/63116419 - running_in_docker_flag = os.getenv("ZAPPA_RUNNING_IN_DOCKER", "False").lower() in {"y", "yes", "t", "true", "1"} + running_in_docker_flag = os.getenv("ZAPPA_RUNNING_IN_DOCKER", "False").lower() in { + "y", + "yes", + "t", + "true", + "1", + } return running_in_docker_flag -SUPPORTED_VERSIONS = [(3, 9), (3, 10), (3, 11), (3, 12), (3, 13)] +SUPPORTED_VERSIONS = [(3, 9), (3, 10), (3, 11), (3, 12), (3, 13), (3, 14)] MINIMUM_SUPPORTED_MINOR_VERSION = 9 diff --git a/zappa/cli.py b/zappa/cli.py index 4153537e3..090c473ce 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -28,6 +28,7 @@ import botocore import click import hjson as json + # import pkg_resources import requests import slugify @@ -1940,7 +1941,13 @@ def init(self, settings_file: str = "zappa_settings.json"): "s3_bucket": bucket, "runtime": get_venv_from_python_version(), "project_name": self.get_project_name(), - "exclude": ["boto3", "dateutil", "botocore", "s3transfer", "concurrent"], + "exclude": [ + "boto3", + "dateutil", + "botocore", + "s3transfer", + "concurrent", + ], } } @@ -2165,7 +2172,10 @@ def certify(self, no_confirm=True, manual=False): if self.use_function_url: self.lambda_arn = self.zappa.get_lambda_function(function_name=self.lambda_name) dns_name = self.zappa.update_lambda_function_url_domains( - self.lambda_arn, self.function_url_domains, cert_arn, self.function_url_cloudfront_config + self.lambda_arn, + self.function_url_domains, + cert_arn, + self.function_url_cloudfront_config, ) if route53: for domain in self.function_url_domains: @@ -3094,6 +3104,7 @@ def touch_endpoint(self, endpoint_url): if req.status_code == 200: click.echo(req.text) + #################################################################### # Main #################################################################### diff --git a/zappa/core.py b/zappa/core.py index 60c5cc6ae..8a9f7c7f5 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1,8 +1,8 @@ """ Zappa core library. You may also want to look at `cli.py` and `util.py`. """ -import datetime +import datetime import getpass import hashlib import json @@ -249,7 +249,7 @@ def build_manylinux_wheel_file_match_pattern(runtime: str, architecture: str) -> # Support PEP600 (https://peps.python.org/pep-0600/) # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl runtime_major_version, runtime_minor_version = runtime[6:].split(".") - python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.13 -> cp313 + python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.14 -> cp314 manylinux_legacy_tags = ("manylinux2014", "manylinux2010", "manylinux1") if architecture == X86_ARCHITECTURE: valid_platform_tags = [X86_ARCHITECTURE] @@ -314,7 +314,7 @@ def __init__( load_credentials=True, desired_role_name=None, desired_role_arn=None, - runtime="python3.13", # Detected at runtime in CLI + runtime="python3.14", # Detected at runtime in CLI tags=(), endpoint_urls={}, xray_tracing=False, @@ -468,7 +468,10 @@ def get_deps_list(self, pkg_name: str, installed_distros: Optional[Iterable] = N for ( requirement_package_name ) in distribution_package.requires: # Generated requirements specified for this Distribution - deps += self.get_deps_list(pkg_name=requirement_package_name, installed_distros=installed_distros) + deps += self.get_deps_list( + pkg_name=requirement_package_name, + installed_distros=installed_distros, + ) return list(set(deps)) # de-dupe before returning def create_handler_venv(self, use_zappa_release: Optional[str] = None): @@ -730,7 +733,12 @@ def splitpath(path): ignore=shutil.ignore_patterns(*excludes), ) else: - copytree(site_packages_64.resolve(), temp_package_path.resolve(), metadata=False, symlinks=False) + copytree( + site_packages_64.resolve(), + temp_package_path.resolve(), + metadata=False, + symlinks=False, + ) if egg_links: self.copy_editable_packages(egg_links, temp_package_path) @@ -1135,7 +1143,7 @@ def create_lambda_function( publish=True, vpc_config=None, dead_letter_config=None, - runtime="python3.13", + runtime="python3.14", aws_environment_variables=None, aws_kms_key_arn=None, snap_start=None, @@ -1328,7 +1336,7 @@ def update_lambda_configuration( ephemeral_storage={"Size": 512}, publish=True, vpc_config=None, - runtime="python3.13", + runtime="python3.14", aws_environment_variables=None, aws_kms_key_arn=None, layers=None, @@ -1589,7 +1597,8 @@ def update_lambda_function_url(self, function_name, function_url_config): ) else: response = self.lambda_client.update_function_url_config( - FunctionName=function_name, AuthType=function_url_config["authorizer"] + FunctionName=function_name, + AuthType=function_url_config["authorizer"], ) print("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(config["FunctionArn"], function_url_config) @@ -1620,7 +1629,10 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains config = { "CallerReference": "zappa-create-function-url-custom-domain-" + function_name.split(":")[-1], - "Aliases": {"Quantity": len(function_url_domains), "Items": function_url_domains}, + "Aliases": { + "Quantity": len(function_url_domains), + "Items": function_url_domains, + }, "DefaultRootObject": "", "Enabled": True, "PriceClass": "PriceClass_100", @@ -1636,7 +1648,10 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains "CustomHeaders": { "Quantity": 1, "Items": [ - {"HeaderName": "CloudFront", "HeaderValue": "CloudFront"}, + { + "HeaderName": "CloudFront", + "HeaderValue": "CloudFront", + }, ], }, "CustomOriginConfig": { @@ -1661,13 +1676,29 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains "FieldLevelEncryptionId": "", "AllowedMethods": { "Quantity": 7, - "Items": ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"], - "CachedMethods": {"Quantity": 3, "Items": ["HEAD", "GET", "OPTIONS"]}, + "Items": [ + "HEAD", + "DELETE", + "POST", + "GET", + "OPTIONS", + "PUT", + "PATCH", + ], + "CachedMethods": { + "Quantity": 3, + "Items": ["HEAD", "GET", "OPTIONS"], + }, }, "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", # noqa: E501 https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac", # noqa: E501 https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-except-host-header }, - "Logging": {"Enabled": False, "IncludeCookies": False, "Bucket": "", "Prefix": ""}, + "Logging": { + "Enabled": False, + "IncludeCookies": False, + "Bucket": "", + "Prefix": "", + }, "Restrictions": {"GeoRestriction": {"RestrictionType": "none", **NULL_CONFIG}}, "WebACLId": "", } @@ -2893,7 +2924,11 @@ def update_domain_name( "path": "/certificateName", "value": certificate_name, }, - {"op": "replace", "path": "/certificateArn", "value": certificate_arn}, + { + "op": "replace", + "path": "/certificateArn", + "value": certificate_arn, + }, ], ) if use_function_url: @@ -3058,15 +3093,9 @@ def _clear_policy(self, lambda_name): for s in statement: if s["Sid"].startswith("zappa-"): logger.debug(f"delete policy {s['Sid']}-{s['Principal']}") - delete_response = self.lambda_client.remove_permission( - FunctionName=lambda_name, StatementId=s["Sid"] - ) + delete_response = self.lambda_client.remove_permission(FunctionName=lambda_name, StatementId=s["Sid"]) if delete_response["ResponseMetadata"]["HTTPStatusCode"] != 204: - logger.error( - "Failed to delete an obsolete policy statement: {}".format( - policy_response - ) - ) + logger.error("Failed to delete an obsolete policy statement: {}".format(policy_response)) else: logger.debug("Failed to load Lambda function policy: {}".format(policy_response)) except ClientError as e: @@ -3090,10 +3119,7 @@ def create_event_permission(self, lambda_name, principal, source_arn): permission_response = self.lambda_client.add_permission( FunctionName=lambda_name, - StatementId="zappa-" - + "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(8) - ), + StatementId="zappa-" + "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)), Action="lambda:InvokeFunction", Principal=principal, SourceArn=source_arn, diff --git a/zappa/utilities.py b/zappa/utilities.py index e0b083bc7..01ab3d634 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -225,6 +225,8 @@ def get_runtime_from_python_version() -> str: return "python3.12" elif sys.version_info[1] == 13: return "python3.13" + elif sys.version_info[1] == 14: + return "python3.14" else: raise ValueError(f"Python f{'.'.join(str(v) for v in sys.version_info[:2])} is not yet supported.") @@ -552,7 +554,10 @@ def remove(self, function_arn: str) -> bool: # Only remove Lambda permission if we actually had a subscription if subscription_removed: try: - self._lambda.remove_permission(FunctionName=function_arn, StatementId=f"sns-{self.arn.split(':')[-1]}") + self._lambda.remove_permission( + FunctionName=function_arn, + StatementId=f"sns-{self.arn.split(':')[-1]}", + ) except Exception as e: LOG.warning(f"Failed to remove Lambda permission for SNS event source {self.arn}: {e.args}") @@ -678,7 +683,11 @@ def update(self, function_arn: str) -> None: def get_event_source( - event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False + event_source: Dict[str, Any], + lambda_arn: str, + target_function: str, + boto_session: boto3.Session, + dry: bool = False, ) -> Tuple[BaseEventSource, str]: """ Given an event_source dictionary item, a session and a lambda_arn, @@ -716,7 +725,11 @@ def get_event_source( def add_event_source( - event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False + event_source: Dict[str, Any], + lambda_arn: str, + target_function: str, + boto_session: boto3.Session, + dry: bool = False, ) -> str: """ Given an event_source dictionary, create the object and add the event source. @@ -734,7 +747,11 @@ def add_event_source( def remove_event_source( - event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False + event_source: Dict[str, Any], + lambda_arn: str, + target_function: str, + boto_session: boto3.Session, + dry: bool = False, ) -> Union[BaseEventSource, bool, Dict[str, Any], None]: """ Given an event_source dictionary, create the object and remove the event source. @@ -749,7 +766,11 @@ def remove_event_source( def get_event_source_status( - event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False + event_source: Dict[str, Any], + lambda_arn: str, + target_function: str, + boto_session: boto3.Session, + dry: bool = False, ) -> Optional[Dict[str, Any]]: """ Given an event_source dictionary, create the object and get the event source status. From 8411c8b1e76a91bb96c3e7e9b2ce595c7aed4d1a Mon Sep 17 00:00:00 2001 From: sha Date: Sun, 19 Oct 2025 11:48:34 +0800 Subject: [PATCH 082/111] Update cli.py --- zappa/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index 090c473ce..eaa454c84 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -2342,7 +2342,6 @@ def load_settings(self, settings_file=None, session=None): dead_letter_arn = self.stage_config.get("dead_letter_arn", "") self.dead_letter_config = {"TargetArn": dead_letter_arn} if dead_letter_arn else {} self.cognito = self.stage_config.get("cognito", None) - self.architecture = [self.stage_config.get("architecture", "x86_64")] self.num_retained_versions = self.stage_config.get("num_retained_versions", None) self.architecture = self.stage_config.get("architecture", "x86_64") # Check for valid values of num_retained_versions @@ -2422,7 +2421,7 @@ def load_settings(self, settings_file=None, session=None): self.tags = self.stage_config.get("tags", {}) # Architectures - self.architecture = [self.stage_config.get("architecture", "x86_64")] + self.architecture = self.stage_config.get("architecture", "x86_64") desired_role_name = self.lambda_name + "-ZappaLambdaExecutionRole" self.zappa = Zappa( From fa164647c6a38bbf26a630d203e1fa8656eecad2 Mon Sep 17 00:00:00 2001 From: sha Date: Sun, 19 Oct 2025 21:47:42 +0800 Subject: [PATCH 083/111] Update core.py --- zappa/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/core.py b/zappa/core.py index 8a9f7c7f5..c0a2a853c 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1258,7 +1258,7 @@ def update_lambda_function( """ logger.info("Updating Lambda function code..") - kwargs = dict(FunctionName=function_name, Publish=publish, Architectures=architecture) + kwargs = dict(FunctionName=function_name, Publish=publish, Architectures=[architecture]) if docker_image_uri: kwargs["ImageUri"] = docker_image_uri elif local_zip: From 1d51424a6a40d132421754b66c4005c9578313bf Mon Sep 17 00:00:00 2001 From: sha Date: Sun, 19 Oct 2025 21:59:04 +0800 Subject: [PATCH 084/111] Update core.py --- zappa/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zappa/core.py b/zappa/core.py index c0a2a853c..412df6718 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1602,8 +1602,9 @@ def update_lambda_function_url(self, function_name, function_url_config): ) print("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(config["FunctionArn"], function_url_config) + return response["FunctionUrl"] else: - self.deploy_lambda_function_url(function_name, function_url_config) + return self.deploy_lambda_function_url(function_name, function_url_config) def delete_lambda_function_url(self, function_name): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) From 411c4d2cde9368a1f2734eef2819cd881d10b0d1 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 3 Nov 2025 15:47:36 +0800 Subject: [PATCH 085/111] aws lambda now requires additional permission for function url: InvokeFunction --- README.md | 2 +- example/policy/deploy.json | 1 + tests/test_core.py | 58 +++++++++++++++++++++++++++----------- zappa/core.py | 33 +++++++++++++++------- 4 files changed, 66 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f4c1d0b7c..865f97650 100644 --- a/README.md +++ b/README.md @@ -1343,7 +1343,7 @@ the `profile_name` setting, which will correspond to a profile in your AWS crede The default IAM policy created by Zappa for executing the Lambda is very permissive. It grants access to all actions for -all resources for types CloudWatch, S3, Kinesis, SNS, SQS, DynamoDB, and Route53; lambda:InvokeFunction +all resources for types CloudWatch, S3, Kinesis, SNS, SQS, DynamoDB, and Route53; lambda:InvokeFunction and lambda:InvokeFunctionUrl for all Lambda resources; Put to all X-Ray resources; and all Network Interface operations to all EC2 resources. While this allows most Lambdas to work correctly with no extra permissions, it is generally not an acceptable set of permissions for most continuous integration pipelines or diff --git a/example/policy/deploy.json b/example/policy/deploy.json index 2130d20be..1a4bf89e1 100644 --- a/example/policy/deploy.json +++ b/example/policy/deploy.json @@ -37,6 +37,7 @@ "lambda:GetFunctionConfiguration", "lambda:GetPolicy", "lambda:InvokeFunction", + "lambda:InvokeFunctionUrl", "lambda:ListVersionsByFunction", "lambda:RemovePermission", "lambda:UpdateFunctionCode", diff --git a/tests/test_core.py b/tests/test_core.py index fede6b81a..e701e31ba 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2839,13 +2839,23 @@ def test_deploy_lambda_function_url(self, client): }, ) - boto_mock.client().add_permission.assert_called_with( - FunctionName=function_name, - StatementId="FunctionURLAllowPublicAccess", - Action="lambda:InvokeFunctionUrl", - Principal="*", - FunctionUrlAuthType=function_url_config["authorizer"], - ) + expected_permission_calls = [ + mock.call( + FunctionName=function_name, + StatementId="FunctionURLAllowPublicAccess", + Action="lambda:InvokeFunctionUrl", + Principal="*", + FunctionUrlAuthType=function_url_config["authorizer"], + ), + mock.call( + FunctionName=function_name, + StatementId="FunctionURLAllowPublicAccessInvoke", + Action="lambda:InvokeFunction", + Principal="*", + ), + ] + boto_mock.client().add_permission.assert_has_calls(expected_permission_calls) + self.assertEqual(boto_mock.client().add_permission.call_count, 2) @mock.patch("botocore.client") def test_update_lambda_function_url(self, client): @@ -2891,7 +2901,9 @@ def test_update_lambda_function_url(self, client): "ResponseMetadata": { "HTTPStatusCode": 200, }, - "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[' + '{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunctionUrl","Resource":""},' + '{"Sid":"FunctionURLAllowPublicAccessInvoke","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', } zappa_core.update_lambda_function_url(function_name="abc", function_url_config=function_url_config) @@ -2958,11 +2970,15 @@ def test_update_lambda_function_url_iam_authorizer(self, client): "ResponseMetadata": { "HTTPStatusCode": 200, }, - "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[' + '{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunctionUrl","Resource":""},' + '{"Sid":"FunctionURLAllowPublicAccessInvoke","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', } zappa_core.lambda_client.remove_permission.return_value = { "ResponseMetadata": {"HTTPStatusCode": 200}, - "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":"xxxxx"}]}', + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[' + '{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunctionUrl","Resource":"xxxxx"},' + '{"Sid":"FunctionURLAllowPublicAccessInvoke","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":"xxxxx"}]}', } zappa_core.update_lambda_function_url(function_name="abc", function_url_config=function_url_config) boto_mock.client().update_function_url_config.assert_called_with( @@ -2981,9 +2997,12 @@ def test_update_lambda_function_url_iam_authorizer(self, client): boto_mock.client().get_policy.assert_called_with( FunctionName=function_arn, ) - boto_mock.client().delete_policy.remove_permission( - FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccess" - ) + expected_remove_calls = [ + mock.call(FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccess"), + mock.call(FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccessInvoke"), + ] + boto_mock.client().remove_permission.assert_has_calls(expected_remove_calls, any_order=True) + self.assertEqual(boto_mock.client().remove_permission.call_count, 2) boto_mock.client().add_permission.assert_not_called() boto_mock.client().create_function_url_config.assert_not_called() @@ -3019,7 +3038,9 @@ def test_delete_lambda_function_url(self, client): "ResponseMetadata": { "HTTPStatusCode": 200, }, - "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', + "Policy": '{"Version":"2012-10-17","Id":"default","Statement":[' + '{"Sid":"FunctionURLAllowPublicAccess","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunctionUrl","Resource":""},' + '{"Sid":"FunctionURLAllowPublicAccessInvoke","Effect":"Allow","Principal":"*","Action":"lambda:InvokeFunction","Resource":""}]}', } zappa_core.lambda_client.remove_permission.return_value = { "ResponseMetadata": { @@ -3035,9 +3056,12 @@ def test_delete_lambda_function_url(self, client): boto_mock.client().get_policy.assert_called_with( FunctionName=function_arn, ) - boto_mock.client().delete_policy.remove_permission( - FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccess" - ) + expected_remove_calls = [ + mock.call(FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccess"), + mock.call(FunctionName=function_arn, StatementId="FunctionURLAllowPublicAccessInvoke"), + ] + boto_mock.client().remove_permission.assert_has_calls(expected_remove_calls, any_order=True) + self.assertEqual(boto_mock.client().remove_permission.call_count, 2) boto_mock.client().add_permission.assert_not_called() boto_mock.client().create_function_url_config.assert_not_called() boto_mock.client().update_function_url_config.assert_not_called() diff --git a/zappa/core.py b/zappa/core.py index 97d982081..4b40afaef 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -86,7 +86,8 @@ { "Effect": "Allow", "Action": [ - "lambda:InvokeFunction" + "lambda:InvokeFunction", + "lambda:InvokeFunctionUrl" ], "Resource": [ "*" @@ -161,6 +162,13 @@ ] }""" +FUNCTION_URL_PUBLIC_PERMISSION_RULES = ( + ("FunctionURLAllowPublicAccess", "lambda:InvokeFunctionUrl", True), + ("FunctionURLAllowPublicAccessInvoke", "lambda:InvokeFunction", False), +) + +FUNCTION_URL_PUBLIC_PERMISSION_SIDS = {rule[0] for rule in FUNCTION_URL_PUBLIC_PERMISSION_RULES} + # Latest list: https://docs.aws.amazon.com/general/latest/gr/rande.html#apigateway_region API_GATEWAY_REGIONS = [ "us-east-1", @@ -1502,7 +1510,7 @@ def list_function_url_policy(self, function_name): if policy_response["ResponseMetadata"]["HTTPStatusCode"] == 200: statement = json.loads(policy_response["Policy"])["Statement"] for s in statement: - if s["Sid"] in ["FunctionURLAllowPublicAccess"]: + if s["Sid"] in FUNCTION_URL_PUBLIC_PERMISSION_SIDS: results.append(s) else: logger.debug("Failed to load Lambda function policy: {}".format(policy_response)) @@ -1522,16 +1530,21 @@ def delete_function_url_policy(self, function_name): def update_function_url_policy(self, function_name, function_url_config): statements = self.list_function_url_policy(function_name) + existing_statement_ids = {statement["Sid"] for statement in statements} if function_url_config["authorizer"] == "NONE": - if not statements: - self.lambda_client.add_permission( - FunctionName=function_name, - StatementId="FunctionURLAllowPublicAccess", - Action="lambda:InvokeFunctionUrl", - Principal="*", - FunctionUrlAuthType=function_url_config["authorizer"], - ) + for sid, action, requires_auth_type in FUNCTION_URL_PUBLIC_PERMISSION_RULES: + if sid in existing_statement_ids: + continue + permission_kwargs = { + "FunctionName": function_name, + "StatementId": sid, + "Action": action, + "Principal": "*", + } + if requires_auth_type: + permission_kwargs["FunctionUrlAuthType"] = function_url_config["authorizer"] + self.lambda_client.add_permission(**permission_kwargs) elif function_url_config["authorizer"] == "AWS_IAM": if statements: self.delete_function_url_policy(function_name) From 8f0c90df532a1304992f5069d370f10b7d6447e4 Mon Sep 17 00:00:00 2001 From: versionbump Date: Fri, 5 Dec 2025 11:43:13 +0800 Subject: [PATCH 086/111] fix url prefix --- zappa/core.py | 7 ------- zappa/handler.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 9f06e15d3..1a2e5f025 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -73,13 +73,6 @@ FUNCTION_URL_PUBLIC_PERMISSION_SIDS = {rule[0] for rule in FUNCTION_URL_PUBLIC_PERMISSION_RULES} -FUNCTION_URL_PUBLIC_PERMISSION_RULES = ( - ("FunctionURLAllowPublicAccess", "lambda:InvokeFunctionUrl", True), - ("FunctionURLAllowPublicAccessInvoke", "lambda:InvokeFunction", False), -) - -FUNCTION_URL_PUBLIC_PERMISSION_SIDS = {rule[0] for rule in FUNCTION_URL_PUBLIC_PERMISSION_RULES} - # Latest list: https://docs.aws.amazon.com/general/latest/gr/rande.html#apigateway_region API_GATEWAY_REGIONS = [ "us-east-1", diff --git a/zappa/handler.py b/zappa/handler.py index d0d40b12d..03fca23a3 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -550,7 +550,7 @@ def handler(self, event, context): if stage: # API Gateway v2 with named stage - rawPath includes the stage script_name = f"/{stage}" - else: + if host.find("lambda-url") > -1: # Function URL - no stage script_name = "" From 78bf5ab5241fb4d2f06976472e82990d9824e1fa Mon Sep 17 00:00:00 2001 From: versionbump Date: Wed, 14 Jan 2026 15:32:26 +0800 Subject: [PATCH 087/111] wip --- README.md | 3 +- tests/test_core.py | 119 +++++++++++++++++++++++++++++++++++++++ tests/test_settings.yaml | 7 +++ zappa/cli.py | 5 ++ zappa/core.py | 43 +++++++++++--- 5 files changed, 169 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cf03157aa..a96c7249a 100644 --- a/README.md +++ b/README.md @@ -1100,7 +1100,8 @@ to change Zappa's behavior. Use these at your own risk! "lambda_description": "Your Description", // However you want to describe your project for the AWS console. Default "Zappa Deployment". "lambda_handler": "your_custom_handler", // The name of Lambda handler. Default: handler.lambda_handler "layers": ["arn:aws:lambda:::layer::"], // optional lambda layers - "lambda_concurrency": 10, // Sets the maximum number of simultaneous executions for a function, and reserves capacity for that concurrency level. Default is None. + "lambda_concurrency": 10, // Sets the maximum number of simultaneous executions for a function, and reserves capacity for that concurrency level. Default is None. Not supported when using capacity providers. + "capacity_provider_config": { "LambdaManagedInstancesCapacityProviderConfig": { "CapacityProviderArn": "arn:aws:lambda:::capacity-provider/", "PerExecutionEnvironmentMaxConcurrency": 10, "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0 } }, // Configure the Lambda capacity provider used for your function. Optional. "lets_encrypt_key": "s3://your-bucket/account.key", // Let's Encrypt account key path. Can either be an S3 path or a local file path. "log_level": "DEBUG", // Set the Zappa log level. Can be one of CRITICAL, ERROR, WARNING, INFO and DEBUG. Default: DEBUG "manage_roles": true, // Have Zappa automatically create and define IAM execution roles and policies. Default true. If false, you must define your own IAM Role and role_name setting. diff --git a/tests/test_core.py b/tests/test_core.py index a3e3e573e..dd69ffa36 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -868,6 +868,32 @@ def test_update_layers(self): z.update_lambda_configuration("test", "test", "test") self.assertEqual(mock_client.update_function_configuration.call_args[1]["Layers"], []) + def test_update_capacity_provider_configuration(self): + z = Zappa(load_credentials=False) + z.credentials_arn = object() + z.lambda_client = mock.MagicMock() + z.lambda_client.get_function_configuration.return_value = {"PackageType": "Zip"} + z.wait_until_lambda_function_is_updated = mock.MagicMock() + capacity_provider_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + "PerExecutionEnvironmentMaxConcurrency": 10, + "ExecutionEnvironmentMemoryGiBPerVCpu": 4.0, + } + } + + z.update_lambda_configuration( + "test", + "test", + "test", + capacity_provider_config=capacity_provider_config, + ) + + self.assertEqual( + z.lambda_client.update_function_configuration.call_args[1]["CapacityProviderConfig"], + capacity_provider_config, + ) + def test_snap_start_configuration(self): """ Test that SnapStart configuration is correctly set in Lambda configuration. @@ -884,6 +910,22 @@ def test_snap_start_configuration(self): zappa_cli.load_settings("tests/test_settings.yaml") self.assertEqual("None", zappa_cli.snap_start) + def test_capacity_provider_configuration(self): + """ + Test that capacity provider configuration is loaded from settings. + """ + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "capacity_provider_enabled" + zappa_cli.load_settings("tests/test_settings.yaml") + expected_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + "PerExecutionEnvironmentMaxConcurrency": 5, + "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0, + } + } + self.assertEqual(expected_config, zappa_cli.capacity_provider_config) + def test_update_empty_aws_env_hash(self): z = Zappa() z.credentials_arn = object() @@ -3439,6 +3481,35 @@ def test_zappa_core_undeploy_lambda_alb(self): elbv2_stubber.activate() zappa_core.undeploy_lambda_alb(**kwargs) + def test_create_lambda_capacity_provider_config(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.credentials_arn = "arn:aws:iam::123:role/zappa" + zappa_core.lambda_client = mock.MagicMock() + zappa_core.wait_until_lambda_function_is_active = mock.MagicMock() + zappa_core.lambda_client.create_function.return_value = { + "FunctionArn": "abc", + "Version": 1, + } + capacity_provider_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + "PerExecutionEnvironmentMaxConcurrency": 3, + "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0, + } + } + + zappa_core.create_lambda_function( + function_name="abc", + handler="handler.lambda_handler", + docker_image_uri="123456789012.dkr.ecr.us-east-1.amazonaws.com/repo:latest", + capacity_provider_config=capacity_provider_config, + ) + + self.assertEqual( + zappa_core.lambda_client.create_function.call_args[1]["CapacityProviderConfig"], + capacity_provider_config, + ) + @mock.patch("botocore.client") def test_set_lambda_concurrency(self, client): boto_mock = mock.MagicMock() @@ -3469,6 +3540,7 @@ def test_update_lambda_concurrency(self, client): aws_region="test", load_credentials=True, ) + zappa_core.lambda_client.get_function_configuration.return_value = {} zappa_core.lambda_client.create_function.return_value = { "FunctionArn": "abc", "Version": 1, @@ -3484,6 +3556,52 @@ def test_update_lambda_concurrency(self, client): ) boto_mock.client().delete_function_concurrency.assert_not_called() + @mock.patch("botocore.client") + def test_update_lambda_concurrency_skipped_for_capacity_provider(self, client): + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + zappa_core.lambda_client.create_function.return_value = { + "FunctionArn": "abc", + "Version": 1, + } + zappa_core.lambda_client.get_function_configuration.return_value = {"CapacityProviderConfig": {"foo": "bar"}} + + zappa_core.update_lambda_function(bucket="test", function_name="abc") + + boto_mock.client().put_function_concurrency.assert_not_called() + boto_mock.client().delete_function_concurrency.assert_not_called() + + def test_create_lambda_capacity_provider_skips_concurrency(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.credentials_arn = "arn:aws:iam::123:role/zappa" + zappa_core.lambda_client = mock.MagicMock() + zappa_core.wait_until_lambda_function_is_active = mock.MagicMock() + zappa_core.lambda_client.create_function.return_value = { + "FunctionArn": "abc", + "Version": 1, + } + capacity_provider_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + } + } + + zappa_core.create_lambda_function( + function_name="abc", + handler="handler.lambda_handler", + bucket="bucket", + s3_key="key", + capacity_provider_config=capacity_provider_config, + concurrency=5, + ) + + zappa_core.lambda_client.put_function_concurrency.assert_not_called() + @mock.patch("botocore.client") def test_delete_lambda_concurrency(self, client): boto_mock = mock.MagicMock() @@ -3493,6 +3611,7 @@ def test_delete_lambda_concurrency(self, client): aws_region="test", load_credentials=True, ) + zappa_core.lambda_client.get_function_configuration.return_value = {} zappa_core.lambda_client.create_function.return_value = { "FunctionArn": "abc", "Version": 1, diff --git a/tests/test_settings.yaml b/tests/test_settings.yaml index 618341168..c5e91dede 100644 --- a/tests/test_settings.yaml +++ b/tests/test_settings.yaml @@ -60,3 +60,10 @@ snap_start_enabled: snap_start_disabled: extends: ttt888 snap_start: None +capacity_provider_enabled: + extends: ttt888 + capacity_provider_config: + LambdaManagedInstancesCapacityProviderConfig: + CapacityProviderArn: arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test + PerExecutionEnvironmentMaxConcurrency: 5 + ExecutionEnvironmentMemoryGiBPerVCpu: 2.0 diff --git a/zappa/cli.py b/zappa/cli.py index 791473887..58783fbe6 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -125,6 +125,7 @@ class ZappaCLI: xray_tracing = False aws_kms_key_arn = "" snap_start = None + capacity_provider_config = None context_header_mappings = None additional_text_mimetypes = None tags = [] # type: ignore[var-annotated] @@ -913,6 +914,7 @@ def deploy(self, source_zip=None, docker_image_uri=None): runtime=self.runtime, aws_environment_variables=self.aws_environment_variables, aws_kms_key_arn=self.aws_kms_key_arn, + capacity_provider_config=self.capacity_provider_config, use_alb=self.use_alb, layers=self.layers, concurrency=self.lambda_concurrency, @@ -1151,6 +1153,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): function_name=self.lambda_name, num_revisions=self.num_retained_versions, concurrency=self.lambda_concurrency, + capacity_provider_config=self.capacity_provider_config, ) if docker_image_uri: kwargs["docker_image_uri"] = docker_image_uri @@ -1196,6 +1199,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): aws_kms_key_arn=self.aws_kms_key_arn, layers=self.layers, snap_start=self.snap_start, + capacity_provider_config=self.capacity_provider_config, wait=False, ) @@ -2679,6 +2683,7 @@ def load_settings(self, settings_file=None, session=None): self.runtime = self.stage_config.get("runtime", get_runtime_from_python_version()) self.aws_kms_key_arn = self.stage_config.get("aws_kms_key_arn", "") self.snap_start = self.stage_config.get("snap_start", "None") + self.capacity_provider_config = self.stage_config.get("capacity_provider_config", None) self.context_header_mappings = self.stage_config.get("context_header_mappings", {}) self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") diff --git a/zappa/core.py b/zappa/core.py index d9c140489..0e2db2e0b 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1210,6 +1210,7 @@ def create_lambda_function( aws_environment_variables=None, aws_kms_key_arn=None, snap_start=None, + capacity_provider_config=None, xray_tracing=False, local_zip=None, use_alb=False, @@ -1236,6 +1237,8 @@ def create_lambda_function( if not layers: layers = [] + uses_capacity_provider = bool(capacity_provider_config) + kwargs = dict( FunctionName=function_name, Role=self.credentials_arn, @@ -1255,6 +1258,8 @@ def create_lambda_function( # zappa currently only supports a single architecture, and uses a str value internally Architectures=[self.architecture], ) + if capacity_provider_config: + kwargs["CapacityProviderConfig"] = capacity_provider_config if not docker_image_uri: kwargs["Runtime"] = runtime kwargs["Handler"] = handler @@ -1291,7 +1296,12 @@ def create_lambda_function( if self.tags: self.lambda_client.tag_resource(Resource=resource_arn, Tags=self.tags) - if concurrency is not None: + if uses_capacity_provider: + if concurrency is not None: + logger.warning( + "Reserved concurrency is not supported with Lambda capacity providers; skipping reserved concurrency." + ) + elif concurrency is not None: self.lambda_client.put_function_concurrency( FunctionName=resource_arn, ReservedConcurrentExecutions=concurrency, @@ -1311,6 +1321,7 @@ def update_lambda_function( local_zip=None, num_revisions=None, concurrency=None, + capacity_provider_config=None, docker_image_uri=None, ): """ @@ -1357,13 +1368,27 @@ def update_lambda_function( Name=ALB_LAMBDA_ALIAS, ) - if concurrency is not None: - self.lambda_client.put_function_concurrency( - FunctionName=function_name, - ReservedConcurrentExecutions=concurrency, - ) + uses_capacity_provider = bool(capacity_provider_config) + if not uses_capacity_provider: + try: + function_configuration = self.lambda_client.get_function_configuration(FunctionName=function_name) + uses_capacity_provider = bool(function_configuration.get("CapacityProviderConfig")) + except botocore.exceptions.ClientError: + uses_capacity_provider = False + + if uses_capacity_provider: + if concurrency is not None: + logger.warning( + "Reserved concurrency is not supported with Lambda capacity providers; skipping reserved concurrency." + ) else: - self.lambda_client.delete_function_concurrency(FunctionName=function_name) + if concurrency is not None: + self.lambda_client.put_function_concurrency( + FunctionName=function_name, + ReservedConcurrentExecutions=concurrency, + ) + else: + self.lambda_client.delete_function_concurrency(FunctionName=function_name) if num_revisions: # Find the existing revision IDs for the given function @@ -1404,6 +1429,7 @@ def update_lambda_configuration( aws_kms_key_arn=None, layers=None, snap_start=None, + capacity_provider_config=None, wait=True, ): """ @@ -1453,6 +1479,9 @@ def update_lambda_configuration( "SnapStart": {"ApplyOn": snap_start if snap_start else "None"}, } + if capacity_provider_config: + kwargs["CapacityProviderConfig"] = capacity_provider_config + if lambda_aws_config.get("PackageType", None) != "Image": kwargs.update( { From 959ecfe66b00ed4724f3aa68b569846fa3561e57 Mon Sep 17 00:00:00 2001 From: versionbump Date: Wed, 14 Jan 2026 15:37:54 +0800 Subject: [PATCH 088/111] handles vpc config --- README.md | 2 +- tests/test_core.py | 34 ++++++++++++++++++++++++++++++++++ zappa/core.py | 14 ++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a96c7249a..e1297e81e 100644 --- a/README.md +++ b/README.md @@ -1101,7 +1101,7 @@ to change Zappa's behavior. Use these at your own risk! "lambda_handler": "your_custom_handler", // The name of Lambda handler. Default: handler.lambda_handler "layers": ["arn:aws:lambda:::layer::"], // optional lambda layers "lambda_concurrency": 10, // Sets the maximum number of simultaneous executions for a function, and reserves capacity for that concurrency level. Default is None. Not supported when using capacity providers. - "capacity_provider_config": { "LambdaManagedInstancesCapacityProviderConfig": { "CapacityProviderArn": "arn:aws:lambda:::capacity-provider/", "PerExecutionEnvironmentMaxConcurrency": 10, "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0 } }, // Configure the Lambda capacity provider used for your function. Optional. + "capacity_provider_config": { "LambdaManagedInstancesCapacityProviderConfig": { "CapacityProviderArn": "arn:aws:lambda:::capacity-provider/", "PerExecutionEnvironmentMaxConcurrency": 10, "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0 } }, // Configure the Lambda capacity provider used for your function. Optional. Not compatible with VPC configurations. "lets_encrypt_key": "s3://your-bucket/account.key", // Let's Encrypt account key path. Can either be an S3 path or a local file path. "log_level": "DEBUG", // Set the Zappa log level. Can be one of CRITICAL, ERROR, WARNING, INFO and DEBUG. Default: DEBUG "manage_roles": true, // Have Zappa automatically create and define IAM execution roles and policies. Default true. If false, you must define your own IAM Role and role_name setting. diff --git a/tests/test_core.py b/tests/test_core.py index dd69ffa36..c39246a90 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -894,6 +894,23 @@ def test_update_capacity_provider_configuration(self): capacity_provider_config, ) + def test_update_capacity_provider_rejects_vpc(self): + z = Zappa(load_credentials=False) + z.credentials_arn = object() + z.lambda_client = mock.MagicMock() + + with self.assertRaises(ValueError): + z.update_lambda_configuration( + "arn", + "name", + "handler", + vpc_config={"SubnetIds": ["subnet-1"], "SecurityGroupIds": ["sg-1"]}, + capacity_provider_config={ + "LambdaManagedInstancesCapacityProviderConfig": {"CapacityProviderArn": "arn:aws:lambda:::capacity"} + }, + wait=False, + ) + def test_snap_start_configuration(self): """ Test that SnapStart configuration is correctly set in Lambda configuration. @@ -3510,6 +3527,23 @@ def test_create_lambda_capacity_provider_config(self): capacity_provider_config, ) + def test_create_lambda_capacity_provider_rejects_vpc(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.credentials_arn = "arn:aws:iam::123:role/zappa" + zappa_core.lambda_client = mock.MagicMock() + + with self.assertRaises(ValueError): + zappa_core.create_lambda_function( + function_name="abc", + handler="handler.lambda_handler", + bucket="bucket", + s3_key="key", + capacity_provider_config={ + "LambdaManagedInstancesCapacityProviderConfig": {"CapacityProviderArn": "arn:aws:lambda:::capacity"} + }, + vpc_config={"SubnetIds": ["subnet-1"], "SecurityGroupIds": ["sg-1"]}, + ) + @mock.patch("botocore.client") def test_set_lambda_concurrency(self, client): boto_mock = mock.MagicMock() diff --git a/zappa/core.py b/zappa/core.py index 0e2db2e0b..b7b9e1012 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1238,6 +1238,12 @@ def create_lambda_function( layers = [] uses_capacity_provider = bool(capacity_provider_config) + uses_vpc = bool(vpc_config and (vpc_config.get("SubnetIds") or vpc_config.get("SecurityGroupIds"))) + if uses_capacity_provider and uses_vpc: + raise ValueError( + "Lambda capacity providers cannot be used with VPC configurations. " + "Remove VPC settings or disable the capacity provider." + ) kwargs = dict( FunctionName=function_name, @@ -1450,6 +1456,14 @@ def update_lambda_configuration( if not layers: layers = [] + uses_capacity_provider = bool(capacity_provider_config) + uses_vpc = bool(vpc_config and (vpc_config.get("SubnetIds") or vpc_config.get("SecurityGroupIds"))) + if uses_capacity_provider and uses_vpc: + raise ValueError( + "Lambda capacity providers cannot be used with VPC configurations. " + "Remove VPC settings or disable the capacity provider." + ) + if wait: # Wait until function is ready, otherwise expected keys will be missing from 'lambda_aws_config'. self.wait_until_lambda_function_is_updated(function_name) From ce2a81b34b437b2f67285748d6c0f5efd1620db8 Mon Sep 17 00:00:00 2001 From: Sha Date: Wed, 14 Jan 2026 17:30:46 +0800 Subject: [PATCH 089/111] efs_config=None, --- zappa/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zappa/core.py b/zappa/core.py index c47bef219..828c0e5a9 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1413,6 +1413,7 @@ def update_lambda_configuration( ephemeral_storage={"Size": 512}, publish=True, vpc_config=None, + efs_config=None, runtime="python3.13", aws_environment_variables=None, aws_kms_key_arn=None, From d40e63ad5ab6000d93dc8006b7120a9cf9a17ceb Mon Sep 17 00:00:00 2001 From: versionbump Date: Wed, 14 Jan 2026 18:25:13 +0800 Subject: [PATCH 090/111] wip --- tests/test_core.py | 38 +++++++++++++++++++++++ zappa/core.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index c39246a90..2791c5c5e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3527,6 +3527,44 @@ def test_create_lambda_capacity_provider_config(self): capacity_provider_config, ) + def test_wait_for_capacity_provider_response(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + expected = {"FunctionVersions": []} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = expected + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + marker="next", + max_items=50, + max_attempts=2, + ) + + zappa_core.lambda_client.list_function_versions_by_capacity_provider.assert_called_with( + CapacityProviderName="provider", + Marker="next", + MaxItems=50, + ) + self.assertEqual(result, expected) + + def test_wait_for_capacity_provider_response_retries(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + error = botocore.exceptions.ClientError( + {"Error": {"Code": "ThrottlingException", "Message": "slow down"}}, "ListFunctionVersionsByCapacityProvider" + ) + success = {"FunctionVersions": [{"FunctionVersion": "1"}]} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.side_effect = [error, success] + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + max_attempts=2, + delay=0, + ) + + self.assertEqual(result, success) + self.assertEqual(zappa_core.lambda_client.list_function_versions_by_capacity_provider.call_count, 2) + def test_create_lambda_capacity_provider_rejects_vpc(self): zappa_core = Zappa(load_credentials=False) zappa_core.credentials_arn = "arn:aws:iam::123:role/zappa" diff --git a/zappa/core.py b/zappa/core.py index b7b9e1012..986c2de6d 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1495,6 +1495,7 @@ def update_lambda_configuration( if capacity_provider_config: kwargs["CapacityProviderConfig"] = capacity_provider_config + kwargs.pop("VpcConfig") if lambda_aws_config.get("PackageType", None) != "Image": kwargs.update( @@ -1508,6 +1509,14 @@ def update_lambda_configuration( response = self.lambda_client.update_function_configuration(**kwargs) resource_arn = response["FunctionArn"] + if capacity_provider_config: + # wait for function to be active, otherwise update function url settings will fail + capacity_provider_arn = capacity_provider_config['LambdaManagedInstancesCapacityProviderConfig']['CapacityProviderArn'] + capacity_provider_name = capacity_provider_arn.split("capacity-provider:", 1)[1] + self.wait_for_capacity_provider_response(f"{response["FunctionArn"]}", capacity_provider_name) + # publish to latest + response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo='LATEST_PUBLISHED') + self.wait_for_capacity_provider_response(f"{response["FunctionArn"]}", capacity_provider_name) if self.tags: self.lambda_client.tag_resource(Resource=resource_arn, Tags=self.tags) @@ -1596,6 +1605,74 @@ def wait_until_lambda_function_is_updated(self, function_name): logger.info(f"Waiting for lambda function [{function_name}] to be updated...") waiter.wait(FunctionName=function_name) + def wait_for_capacity_provider_response(self, function_arn: str, capacity_provider_name: str): + """ + Poll list_function_versions_by_capacity_provider until the target function version is: + - Active -> return the matching entry (and the last full response seen) + - Failed -> raise RuntimeError immediately + """ + max_attempts = 30 + delay_seconds = 3 + def iter_all_function_versions(): + """Yield items across all pages.""" + paginator = self.lambda_client.get_paginator("list_function_versions_by_capacity_provider") + for page in paginator.paginate(CapacityProviderName=capacity_provider_name, PaginationConfig={"PageSize": 50}): + for item in page.get("FunctionVersions", []): + yield item, page + + for attempt in range(1, max_attempts + 1): + try: + # Find matching function version item across pages + found_item = None + last_page = None + + for item, page in iter_all_function_versions(): + + print(item) + last_page = page + arn = item.get("FunctionArn", "") + if arn.find(function_arn) > -1: + found_item = item + break + + # If not found yet, keep polling + if not found_item: + # Optional: print the last page we saw for debugging + # print(json.dumps(last_page or {}, indent=2)) + time.sleep(delay_seconds) + continue + + state = found_item.get("State") + if state == "Active": + # Return something useful: the matched entry + a compact view of the page + return { + "CapacityProviderName": capacity_provider_name, + "MatchedFunction": found_item, + "LastSeenPage": last_page, + } + + if state == "Failed": + raise RuntimeError( + f"Function version [{found_item.get('FunctionArn')}] entered Failed state " + f"under capacity provider [{capacity_provider_name}]." + ) + + # Pending / Deleting / etc: keep polling + print(f"Attempt {attempt}/{max_attempts}: {found_item.get('FunctionArn')} state={state}") + time.sleep(delay_seconds) + + except botocore.exceptions.ClientError as e: + error_code = e.response.get("Error", {}).get("Code") + if error_code in ("ThrottlingException", "TooManyRequestsException") and attempt < max_attempts: + time.sleep(delay_seconds) + continue + raise + + raise TimeoutError( + f"Timed out waiting for function [{function_arn}] to become Active " + f"under capacity provider [{capacity_provider_name}] after {max_attempts} attempts." + ) + def get_lambda_function(self, function_name): """ Returns the lambda function ARN, given a name From 623281f7280fcfd199787b90583ad4fb95810c6d Mon Sep 17 00:00:00 2001 From: versionbump Date: Wed, 14 Jan 2026 20:33:34 +0800 Subject: [PATCH 091/111] wip --- tests/test_core.py | 85 +++++++++++++++++++++-- zappa/core.py | 168 ++++++++++++++++++++++++++++----------------- 2 files changed, 185 insertions(+), 68 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 4aec45b9e..81c754a41 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3543,15 +3543,90 @@ def test_wait_for_capacity_provider_response(self): result = zappa_core.wait_for_capacity_provider_response( capacity_provider_name="provider", - marker="next", - max_items=50, + ) + + zappa_core.lambda_client.list_function_versions_by_capacity_provider.assert_called_with( + CapacityProviderName="provider", + ) + self.assertEqual(result, expected) + + def test_wait_for_capacity_provider_response_waits_until_active(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + function_arn = "arn:aws:lambda:us-east-1:123:function:test:1" + pending = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Pending"}]} + active = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Active"}]} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.side_effect = [pending, active] + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Active", + max_attempts=2, + delay_seconds=0, + ) + + self.assertEqual(result, active) + self.assertEqual(zappa_core.lambda_client.list_function_versions_by_capacity_provider.call_count, 2) + + def test_wait_for_capacity_provider_response_waits_until_empty(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + function_arn = "arn:aws:lambda:us-east-1:123:function:test:1" + present = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Pending"}]} + gone = {"FunctionVersions": []} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.side_effect = [present, gone] + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Empty", max_attempts=2, + delay_seconds=0, + ) + + self.assertEqual(result, gone) + self.assertEqual(zappa_core.lambda_client.list_function_versions_by_capacity_provider.call_count, 2) + + def test_wait_for_capacity_provider_response_exits_on_failed(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + function_arn = "arn:aws:lambda:us-east-1:123:function:test:1" + failed = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Failed"}]} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = failed + + with self.assertRaises(RuntimeError): + zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Active", + max_attempts=1, + delay_seconds=0, + ) + + with self.assertRaises(RuntimeError): + zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Empty", + max_attempts=1, + delay_seconds=0, + ) + + def test_wait_for_capacity_provider_response_passes_marker(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + expected = {"FunctionVersions": []} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = expected + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + marker="m", ) zappa_core.lambda_client.list_function_versions_by_capacity_provider.assert_called_with( CapacityProviderName="provider", - Marker="next", - MaxItems=50, + Marker="m", ) self.assertEqual(result, expected) @@ -3567,7 +3642,7 @@ def test_wait_for_capacity_provider_response_retries(self): result = zappa_core.wait_for_capacity_provider_response( capacity_provider_name="provider", max_attempts=2, - delay=0, + delay_seconds=0, ) self.assertEqual(result, success) diff --git a/zappa/core.py b/zappa/core.py index 7db19dc41..895f50119 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1527,13 +1527,35 @@ def update_lambda_configuration( resource_arn = response["FunctionArn"] if capacity_provider_config: + # wait for function to be active, otherwise update function url settings will fail - capacity_provider_arn = capacity_provider_config['LambdaManagedInstancesCapacityProviderConfig']['CapacityProviderArn'] - capacity_provider_name = capacity_provider_arn.split("capacity-provider:", 1)[1] - self.wait_for_capacity_provider_response(f"{response["FunctionArn"]}", capacity_provider_name) + capacity_provider_arn = capacity_provider_config["LambdaManagedInstancesCapacityProviderConfig"][ + "CapacityProviderArn" + ] + capacity_provider_name_part = capacity_provider_arn + if "capacity-provider/" in capacity_provider_name_part: + capacity_provider_name_part = capacity_provider_name_part.split("capacity-provider/", 1)[1] + elif "capacity-provider:" in capacity_provider_name_part: + capacity_provider_name_part = capacity_provider_name_part.split("capacity-provider:", 1)[1] + capacity_provider_name = capacity_provider_name_part.rsplit("/", 1)[-1] + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=response["FunctionArn"], + function_state="Active", + ) + # if latest version exists, wait to be deleted + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=f"{response["FunctionArn"]}:$LATEST.PUBLISHED", + function_state="Empty", + ) # publish to latest response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo='LATEST_PUBLISHED') - self.wait_for_capacity_provider_response(f"{response["FunctionArn"]}", capacity_provider_name) + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=response["FunctionArn"], + function_state="Active", + ) if self.tags: self.lambda_client.tag_resource(Resource=resource_arn, Tags=self.tags) @@ -1622,72 +1644,92 @@ def wait_until_lambda_function_is_updated(self, function_name): logger.info(f"Waiting for lambda function [{function_name}] to be updated...") waiter.wait(FunctionName=function_name) - def wait_for_capacity_provider_response(self, function_arn: str, capacity_provider_name: str): + def wait_for_capacity_provider_response( + self, + function_arn: str | None = None, + capacity_provider_name: str | None = None, + function_state: str = "Active", + marker: str | None = None, + max_attempts: int = 60, + delay_seconds: float = 5, + ): """ - Poll list_function_versions_by_capacity_provider until the target function version is: - - Active -> return the matching entry (and the last full response seen) - - Failed -> raise RuntimeError immediately + Call list_function_versions_by_capacity_provider with retry. + + If function_arn is provided: + - function_state="Active": poll until the matching FunctionVersion has State=Active + - function_state="Empty": poll until the matching FunctionVersion record is no longer returned + In both modes, if the matching record is ever seen in State=Failed, raises RuntimeError. + Returns the last API response seen. """ - max_attempts = 30 - delay_seconds = 3 - def iter_all_function_versions(): - """Yield items across all pages.""" - paginator = self.lambda_client.get_paginator("list_function_versions_by_capacity_provider") - for page in paginator.paginate(CapacityProviderName=capacity_provider_name, PaginationConfig={"PageSize": 50}): - for item in page.get("FunctionVersions", []): - yield item, page + if not capacity_provider_name: + raise ValueError("capacity_provider_name is required.") + normalized_state = (function_state or "").strip().lower() + if normalized_state not in ("active", "empty"): + raise ValueError("function_state must be 'Active' or 'Empty'.") + if normalized_state == "empty" and not function_arn: + raise ValueError("function_arn is required when function_state='Empty'.") + + list_kwargs = {"CapacityProviderName": capacity_provider_name} + if marker: + list_kwargs["Marker"] = marker + + last_response = None for attempt in range(1, max_attempts + 1): try: - # Find matching function version item across pages - found_item = None - last_page = None + response = self.lambda_client.list_function_versions_by_capacity_provider(**list_kwargs) + last_response = response + except botocore.exceptions.ClientError as e: + error_code = e.response.get("Error", {}).get("Code") + if error_code in ("ThrottlingException", "Throttling", "TooManyRequestsException") and attempt < max_attempts: + time.sleep(delay_seconds) + continue + raise - for item, page in iter_all_function_versions(): + if not function_arn: + return response - print(item) - last_page = page - arn = item.get("FunctionArn", "") - if arn.find(function_arn) > -1: - found_item = item + matched_item = None + page = response or {} + while True: + for item in (page.get("FunctionVersions") or []): + arn = item.get("FunctionArn") or "" + if function_arn in arn: + matched_item = item break + if matched_item: + break - # If not found yet, keep polling - if not found_item: - # Optional: print the last page we saw for debugging - # print(json.dumps(last_page or {}, indent=2)) - time.sleep(delay_seconds) - continue + next_marker = page.get("NextMarker") + if not next_marker: + break - state = found_item.get("State") - if state == "Active": - # Return something useful: the matched entry + a compact view of the page - return { - "CapacityProviderName": capacity_provider_name, - "MatchedFunction": found_item, - "LastSeenPage": last_page, - } + page = self.lambda_client.list_function_versions_by_capacity_provider( + CapacityProviderName=capacity_provider_name, + Marker=next_marker, + ) + last_response = page + if matched_item: + logger.info(f"{attempt}/{max_attempts} attempts: {matched_item}") + state = matched_item.get("State") if state == "Failed": raise RuntimeError( - f"Function version [{found_item.get('FunctionArn')}] entered Failed state " - f"under capacity provider [{capacity_provider_name}]." + f"Function version [{matched_item.get('FunctionArn')}] entered Failed state under " + f"capacity provider [{capacity_provider_name}]." ) + if normalized_state == "active" and state == "Active": + return last_response + else: + if normalized_state == "empty": + return last_response - # Pending / Deleting / etc: keep polling - print(f"Attempt {attempt}/{max_attempts}: {found_item.get('FunctionArn')} state={state}") - time.sleep(delay_seconds) - - except botocore.exceptions.ClientError as e: - error_code = e.response.get("Error", {}).get("Code") - if error_code in ("ThrottlingException", "TooManyRequestsException") and attempt < max_attempts: - time.sleep(delay_seconds) - continue - raise + time.sleep(delay_seconds) raise TimeoutError( - f"Timed out waiting for function [{function_arn}] to become Active " - f"under capacity provider [{capacity_provider_name}] after {max_attempts} attempts." + f"Timed out waiting for function [{function_arn}] to become {function_state} under capacity provider " + f"[{capacity_provider_name}] after {max_attempts} attempts." ) def get_lambda_function(self, function_name): @@ -1786,7 +1828,7 @@ def deploy_lambda_function_url(self, function_name, function_url_config): response = self.lambda_client.create_function_url_config( FunctionName=function_name, AuthType=function_url_config["authorizer"] ) - print("function URL address: {}".format(response["FunctionUrl"])) + logger.info("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(function_name, function_url_config) return response @@ -1812,7 +1854,7 @@ def update_lambda_function_url(self, function_name, function_url_config): FunctionName=function_name, AuthType=function_url_config["authorizer"], ) - print("function URL address: {}".format(response["FunctionUrl"])) + logger.info("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(config["FunctionArn"], function_url_config) return response["FunctionUrl"] else: @@ -1823,7 +1865,7 @@ def delete_lambda_function_url(self, function_name): for config in response.get("FunctionUrlConfigs", []): resp = self.lambda_client.delete_function_url_config(FunctionName=config["FunctionArn"]) if resp["ResponseMetadata"]["HTTPStatusCode"] == 204: - print("function URL deleted: {}".format(config["FunctionUrl"])) + logger.info("function URL deleted: {}".format(config["FunctionUrl"])) self.delete_function_url_policy(config["FunctionArn"]) ## @@ -1832,7 +1874,7 @@ def delete_lambda_function_url(self, function_name): def update_lambda_function_url_domains(self, function_name, function_url_domains, certificate_arn, cloudfront_config): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) if not response.get("FunctionUrlConfigs", []): - print("no function url configured on lambda, skip setting custom domains") + logger.info("no function url configured on lambda, skip setting custom domains") url = response["FunctionUrlConfigs"][0]["FunctionUrl"] import urllib @@ -1933,7 +1975,7 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains if not distributions: response = self.cloudfront_client.create_distribution(DistributionConfig=config) if response["ResponseMetadata"]["HTTPStatusCode"] == 201: - print( + logger.info( "created cloudfront distribution for {}. It will take a while for the change to be deployed.".format( function_url_domains ) @@ -1951,7 +1993,7 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains DistributionConfig=new_config, Id=id, IfMatch=distribution["ETag"] ) if response["ResponseMetadata"]["HTTPStatusCode"] == 200: - print( + logger.info( "update cloudfront distribution for {}. It will take a while for the change to be deployed.".format( function_url_domains ) @@ -2714,7 +2756,7 @@ def undeploy_function_url_custom_domain(self, lambda_name, domains=None): response = self.lambda_client.list_function_url_configs(FunctionName=lambda_name, MaxItems=50) if not response.get("FunctionUrlConfigs", []): - print("no function url configured on lambda. skip delete custom domains") + logger.info("no function url configured on lambda. skip delete custom domains") url = response["FunctionUrlConfigs"][0]["FunctionUrl"] url = urllib.parse.urlparse(url) distributions = self.cloudfront_client.list_distributions() @@ -2734,12 +2776,12 @@ def undeploy_function_url_custom_domain(self, lambda_name, domains=None): DistributionConfig=new_config, Id=id, IfMatch=distribution["ETag"] ) response - print("wait for distribution to be disabled.") + logger.info("wait for distribution to be disabled.") waiter = self.cloudfront_client.get_waiter("distribution_deployed") waiter.wait(Id=id, WaiterConfig={"Delay": 20, "MaxAttempts": 20}) delete_response = self.cloudfront_client.delete_distribution(Id=id, IfMatch=response["ETag"]) if delete_response["ResponseMetadata"]["HTTPStatusCode"] == 204: - print("cloudfront distribution deleted") + logger.info("cloudfront distribution deleted") # attempt remove 53 record for domain in distribution["Distribution"]["DistributionConfig"]["Aliases"].get("Items", []): @@ -3199,7 +3241,7 @@ def delete_route53_records(self, domain_name, dns_name): ) if response["ResponseMetadata"]["HTTPStatusCode"] == 200: - print("removed route 53 record for {}".format(domain_name)) + logger.info("removed route 53 record for {}".format(domain_name)) return response From 4a02dd9edab4d99f75f8d3353a2e0fdd7f26d590 Mon Sep 17 00:00:00 2001 From: versionbump Date: Wed, 14 Jan 2026 20:46:56 +0800 Subject: [PATCH 092/111] add logging message --- zappa/core.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 895f50119..1214ec601 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1646,8 +1646,8 @@ def wait_until_lambda_function_is_updated(self, function_name): def wait_for_capacity_provider_response( self, - function_arn: str | None = None, - capacity_provider_name: str | None = None, + function_arn: str, + capacity_provider_name: str, function_state: str = "Active", marker: str | None = None, max_attempts: int = 60, @@ -1668,8 +1668,6 @@ def wait_for_capacity_provider_response( normalized_state = (function_state or "").strip().lower() if normalized_state not in ("active", "empty"): raise ValueError("function_state must be 'Active' or 'Empty'.") - if normalized_state == "empty" and not function_arn: - raise ValueError("function_arn is required when function_state='Empty'.") list_kwargs = {"CapacityProviderName": capacity_provider_name} if marker: @@ -1679,6 +1677,7 @@ def wait_for_capacity_provider_response( for attempt in range(1, max_attempts + 1): try: response = self.lambda_client.list_function_versions_by_capacity_provider(**list_kwargs) + logger.info(f"Function Versions: {response}") last_response = response except botocore.exceptions.ClientError as e: error_code = e.response.get("Error", {}).get("Code") @@ -1711,8 +1710,8 @@ def wait_for_capacity_provider_response( ) last_response = page + logger.info(f"{attempt}/{max_attempts} attempts: {normalized_state=}, {matched_item=}") if matched_item: - logger.info(f"{attempt}/{max_attempts} attempts: {matched_item}") state = matched_item.get("State") if state == "Failed": raise RuntimeError( From 344ecfa3bf692059cc2e98e5540a0795a1143f9d Mon Sep 17 00:00:00 2001 From: versionbump Date: Wed, 14 Jan 2026 20:54:40 +0800 Subject: [PATCH 093/111] logging enhancement --- zappa/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 1214ec601..6b3cf2b26 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1677,7 +1677,7 @@ def wait_for_capacity_provider_response( for attempt in range(1, max_attempts + 1): try: response = self.lambda_client.list_function_versions_by_capacity_provider(**list_kwargs) - logger.info(f"Function Versions: {response}") + logger.info(json.dumps(response, indent=2)) last_response = response except botocore.exceptions.ClientError as e: error_code = e.response.get("Error", {}).get("Code") @@ -1710,7 +1710,7 @@ def wait_for_capacity_provider_response( ) last_response = page - logger.info(f"{attempt}/{max_attempts} attempts: {normalized_state=}, {matched_item=}") + logger.info(f"{attempt}/{max_attempts} attempts: {function_arn=} {normalized_state=}, {matched_item=}") if matched_item: state = matched_item.get("State") if state == "Failed": From 03f71569b705b518c6f1e1cf67c84a1adb1f53d9 Mon Sep 17 00:00:00 2001 From: versionbump Date: Thu, 15 Jan 2026 09:38:07 +0800 Subject: [PATCH 094/111] fix managed capacity deployment --- zappa/core.py | 53 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 6b3cf2b26..54d7f4a65 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1414,17 +1414,11 @@ def update_lambda_function( if num_revisions: # Find the existing revision IDs for the given function # Related: https://github.com/Miserlou/Zappa/issues/1402 - versions_in_lambda = [] - versions = self.lambda_client.list_versions_by_function(FunctionName=function_name) - for version in versions["Versions"]: - versions_in_lambda.append(version["Version"]) - while "NextMarker" in versions: - versions = self.lambda_client.list_versions_by_function( - FunctionName=function_name, Marker=versions["NextMarker"] - ) - for version in versions["Versions"]: - versions_in_lambda.append(version["Version"]) + versions_in_lambda = self.list_lambda_function_versions(function_name) versions_in_lambda.remove("$LATEST") + + if "$LATEST.PUBLISHED" in versions_in_lambda: + versions_in_lambda.remove("$LATEST.PUBLISHED") # Delete older revisions if their number exceeds the specified limit for version in versions_in_lambda[::-1][num_revisions:]: self.lambda_client.delete_function(FunctionName=function_name, Qualifier=version) @@ -1433,6 +1427,19 @@ def update_lambda_function( return resource_arn + def list_lambda_function_versions(self, function_name): + versions_in_lambda = [] + versions = self.lambda_client.list_versions_by_function(FunctionName=function_name) + for version in versions["Versions"]: + versions_in_lambda.append(version["Version"]) + while "NextMarker" in versions: + versions = self.lambda_client.list_versions_by_function( + FunctionName=function_name, Marker=versions["NextMarker"] + ) + for version in versions["Versions"]: + versions_in_lambda.append(version["Version"]) + return versions_in_lambda + def update_lambda_configuration( self, lambda_arn, @@ -1527,8 +1534,7 @@ def update_lambda_configuration( resource_arn = response["FunctionArn"] if capacity_provider_config: - - # wait for function to be active, otherwise update function url settings will fail + capacity_provider_arn = capacity_provider_config["LambdaManagedInstancesCapacityProviderConfig"][ "CapacityProviderArn" ] @@ -1538,19 +1544,21 @@ def update_lambda_configuration( elif "capacity-provider:" in capacity_provider_name_part: capacity_provider_name_part = capacity_provider_name_part.split("capacity-provider:", 1)[1] capacity_provider_name = capacity_provider_name_part.rsplit("/", 1)[-1] + # wait for latest version + versions_in_lambda = self.list_lambda_function_versions(function_name=function_name) + latest_version = max(int(v) for v in versions_in_lambda if v.isdigit()) + self.wait_for_capacity_provider_response( capacity_provider_name=capacity_provider_name, - function_arn=response["FunctionArn"], + function_arn=f"{response["FunctionArn"]}:{latest_version}", function_state="Active", ) - # if latest version exists, wait to be deleted - self.wait_for_capacity_provider_response( - capacity_provider_name=capacity_provider_name, - function_arn=f"{response["FunctionArn"]}:$LATEST.PUBLISHED", - function_state="Empty", - ) + # publish to latest response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo='LATEST_PUBLISHED') + logger.info(f"Publish to {response['FunctionArn']}") + + time.sleep(10) self.wait_for_capacity_provider_response( capacity_provider_name=capacity_provider_name, function_arn=response["FunctionArn"], @@ -1673,11 +1681,12 @@ def wait_for_capacity_provider_response( if marker: list_kwargs["Marker"] = marker + + logger.info(f"Wait for {function_arn} to be {function_state} ...") last_response = None for attempt in range(1, max_attempts + 1): try: response = self.lambda_client.list_function_versions_by_capacity_provider(**list_kwargs) - logger.info(json.dumps(response, indent=2)) last_response = response except botocore.exceptions.ClientError as e: error_code = e.response.get("Error", {}).get("Code") @@ -1710,7 +1719,7 @@ def wait_for_capacity_provider_response( ) last_response = page - logger.info(f"{attempt}/{max_attempts} attempts: {function_arn=} {normalized_state=}, {matched_item=}") + logger.info(f"{attempt}/{max_attempts} attempts: current state {matched_item.get("State")}, expect {function_state}") if matched_item: state = matched_item.get("State") if state == "Failed": @@ -1718,7 +1727,7 @@ def wait_for_capacity_provider_response( f"Function version [{matched_item.get('FunctionArn')}] entered Failed state under " f"capacity provider [{capacity_provider_name}]." ) - if normalized_state == "active" and state == "Active": + if normalized_state == "active" and state == 'Active': return last_response else: if normalized_state == "empty": From de80af90f22cb9f84f569ea3d36f6e4b374b068b Mon Sep 17 00:00:00 2001 From: versionbump Date: Thu, 15 Jan 2026 11:16:16 +0800 Subject: [PATCH 095/111] update deploy script --- zappa/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zappa/core.py b/zappa/core.py index 54d7f4a65..79ffcf6c3 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1216,6 +1216,7 @@ def create_lambda_function( ephemeral_storage={"Size": 512}, publish=True, vpc_config=None, + efs_config=None, dead_letter_config=None, runtime="python3.13", aws_environment_variables=None, @@ -1280,6 +1281,7 @@ def create_lambda_function( ) if capacity_provider_config: kwargs["CapacityProviderConfig"] = capacity_provider_config + kwargs.pop("VpcConfig") if not docker_image_uri: kwargs["Runtime"] = runtime kwargs["Handler"] = handler @@ -1719,9 +1721,9 @@ def wait_for_capacity_provider_response( ) last_response = page - logger.info(f"{attempt}/{max_attempts} attempts: current state {matched_item.get("State")}, expect {function_state}") if matched_item: state = matched_item.get("State") + logger.info(f"{attempt}/{max_attempts} attempts: current state {state}, expect {function_state}") if state == "Failed": raise RuntimeError( f"Function version [{matched_item.get('FunctionArn')}] entered Failed state under " From ea04c84d9a087bbadd91fd95084458296e19dba9 Mon Sep 17 00:00:00 2001 From: versionbump Date: Fri, 23 Jan 2026 11:27:40 +0800 Subject: [PATCH 096/111] Update core.py --- zappa/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zappa/core.py b/zappa/core.py index 79ffcf6c3..b5d33fb43 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -324,6 +324,7 @@ def __init__( self.dynamodb_client = self.boto_client("dynamodb") self.cognito_client = self.boto_client("cognito-idp") self.sts_client = self.boto_client("sts") + self.cloudfront_client = self.boto_client("cloudfront") self.tags = tags self.cf_template = troposphere.Template() From b50af0edb7e73aa349df909e604a5f92f097a30a Mon Sep 17 00:00:00 2001 From: versionbump Date: Fri, 23 Jan 2026 13:07:08 +0800 Subject: [PATCH 097/111] wip: enable managed capacity for lambda --- README.md | 3 +- .../cloudfront.DeleteDistribution_1.json | 12 + .../cloudfront.UpdateDistribution_1.json | 162 +++++++ .../route53.ChangeResourceRecordSets_1.json | 1 + tests/test_core.py | 285 +++++++++++++ tests/test_settings.yaml | 7 + tests/test_wsgi_script_name_settings.py | 1 + zappa/cli.py | 121 ++++-- zappa/core.py | 396 ++++++++++++++---- zappa/handler.py | 15 +- 10 files changed, 885 insertions(+), 118 deletions(-) create mode 100644 tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json create mode 100644 tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json create mode 100644 tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json diff --git a/README.md b/README.md index cf03157aa..e1297e81e 100644 --- a/README.md +++ b/README.md @@ -1100,7 +1100,8 @@ to change Zappa's behavior. Use these at your own risk! "lambda_description": "Your Description", // However you want to describe your project for the AWS console. Default "Zappa Deployment". "lambda_handler": "your_custom_handler", // The name of Lambda handler. Default: handler.lambda_handler "layers": ["arn:aws:lambda:::layer::"], // optional lambda layers - "lambda_concurrency": 10, // Sets the maximum number of simultaneous executions for a function, and reserves capacity for that concurrency level. Default is None. + "lambda_concurrency": 10, // Sets the maximum number of simultaneous executions for a function, and reserves capacity for that concurrency level. Default is None. Not supported when using capacity providers. + "capacity_provider_config": { "LambdaManagedInstancesCapacityProviderConfig": { "CapacityProviderArn": "arn:aws:lambda:::capacity-provider/", "PerExecutionEnvironmentMaxConcurrency": 10, "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0 } }, // Configure the Lambda capacity provider used for your function. Optional. Not compatible with VPC configurations. "lets_encrypt_key": "s3://your-bucket/account.key", // Let's Encrypt account key path. Can either be an S3 path or a local file path. "log_level": "DEBUG", // Set the Zappa log level. Can be one of CRITICAL, ERROR, WARNING, INFO and DEBUG. Default: DEBUG "manage_roles": true, // Have Zappa automatically create and define IAM execution roles and policies. Default true. If false, you must define your own IAM Role and role_name setting. diff --git a/tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json b/tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json new file mode 100644 index 000000000..ffbb4ac71 --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/cloudfront.DeleteDistribution_1.json @@ -0,0 +1,12 @@ +{ + "ResponseMetadata": { + "RequestId": "7f720a6c-9e5b-4080-b289-df07fe33c7fc", + "HTTPStatusCode": 204, + "HTTPHeaders": { + "x-amzn-requestid": "7f720a6c-9e5b-4080-b289-df07fe33c7fc", + "content-type": "text/xml", + "date": "Wed, 26 Oct 2022 06:16:00 GMT" + }, + "RetryAttempts": 0 + } +} \ No newline at end of file diff --git a/tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json b/tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json new file mode 100644 index 000000000..32ff81426 --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/cloudfront.UpdateDistribution_1.json @@ -0,0 +1,162 @@ +{ + "ResponseMetadata": { + "RequestId": "e9a681a2-2a82-4e8c-bab0-133e6d1fd04d", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "x-amzn-requestid": "e9a681a2-2a82-4e8c-bab0-133e6d1fd04d", + "etag": "EQZU30ULEIL60", + "content-type": "text/xml", + "content-length": "4148", + "date": "Wed, 26 Oct 2022 03:45:13 GMT" + }, + "RetryAttempts": 0 + }, + "ETag": "EQZU30ULEIL60", + "Distribution": { + "Id": "E1YIU775JNY3JV", + "ARN": "arn:aws:cloudfront::123456789:distribution/E1YIU775JNY3JV", + "Status": "InProgress", + "LastModifiedTime": "", + "InProgressInvalidationBatches": 0, + "DomainName": "dolayrplf7f1.cloudfront.net", + "ActiveTrustedSigners": { + "Enabled": false, + "Quantity": 0 + }, + "ActiveTrustedKeyGroups": { + "Enabled": false, + "Quantity": 0 + }, + "DistributionConfig": { + "CallerReference": "zappa-create-function-url-custom-domain", + "Aliases": { + "Quantity": 1, + "Items": [ + "test-lambda-function-url.example.com" + ] + }, + "DefaultRootObject": "", + "Origins": { + "Quantity": 1, + "Items": [ + { + "Id": "LambdaFunctionURL", + "DomainName": "wwvjk2tpuvrr457k3xt4kuryby0qmmzs.lambda-url.ap-southeast-1.on.aws", + "OriginPath": "", + "CustomHeaders": { + "Quantity": 1, + "Items": [ + { + "HeaderName": "CloudFront", + "HeaderValue": "CloudFront" + } + ] + }, + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginProtocolPolicy": "https-only", + "OriginSslProtocols": { + "Quantity": 1, + "Items": [ + "TLSv1" + ] + }, + "OriginReadTimeout": 60, + "OriginKeepaliveTimeout": 60 + }, + "ConnectionAttempts": 3, + "ConnectionTimeout": 10, + "OriginShield": { + "Enabled": false + }, + "OriginAccessControlId": "" + } + ] + }, + "OriginGroups": { + "Quantity": 0 + }, + "DefaultCacheBehavior": { + "TargetOriginId": "LambdaFunctionURL", + "TrustedSigners": { + "Enabled": false, + "Quantity": 0 + }, + "TrustedKeyGroups": { + "Enabled": false, + "Quantity": 0 + }, + "ViewerProtocolPolicy": "redirect-to-https", + "AllowedMethods": { + "Quantity": 7, + "Items": [ + "HEAD", + "DELETE", + "POST", + "GET", + "OPTIONS", + "PUT", + "PATCH" + ], + "CachedMethods": { + "Quantity": 3, + "Items": [ + "HEAD", + "GET", + "OPTIONS" + ] + } + }, + "SmoothStreaming": true, + "Compress": true, + "LambdaFunctionAssociations": { + "Quantity": 0 + }, + "FunctionAssociations": { + "Quantity": 0 + }, + "FieldLevelEncryptionId": "", + "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" + }, + "CacheBehaviors": { + "Quantity": 0 + }, + "CustomErrorResponses": { + "Quantity": 0 + }, + "Comment": "Lambda FunctionURL zappa-function-url-test-dev", + "Logging": { + "Enabled": false, + "IncludeCookies": false, + "Bucket": "", + "Prefix": "" + }, + "PriceClass": "PriceClass_100", + "Enabled": true, + "ViewerCertificate": { + "CloudFrontDefaultCertificate": false, + "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789:certificate/77bff5cb-03c7-4b11-ba8e-312e6f49a31f", + "SSLSupportMethod": "sni-only", + "MinimumProtocolVersion": "TLSv1.2_2021", + "Certificate": "arn:aws:acm:us-east-1:123456789:certificate/77bff5cb-03c7-4b11-ba8e-312e6f49a31f", + "CertificateSource": "acm" + }, + "Restrictions": { + "GeoRestriction": { + "RestrictionType": "none", + "Quantity": 0 + } + }, + "WebACLId": "", + "HttpVersion": "http2", + "IsIPV6Enabled": true + }, + "AliasICPRecordals": [ + { + "CNAME": "test-lambda-function-url.example.com", + "ICPRecordalStatus": "APPROVED" + } + ] + } +} \ No newline at end of file diff --git a/tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json b/tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json new file mode 100644 index 000000000..ff367525f --- /dev/null +++ b/tests/placebo/TestZappa.test_cli_aws/route53.ChangeResourceRecordSets_1.json @@ -0,0 +1 @@ +{'ResponseMetadata': {'RequestId': 'ec2fc714-a609-43ac-977e-2a8612d131a3', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': 'ec2fc714-a609-43ac-977e-2a8612d131a3', 'content-type': 'text/xml', 'content-length': '282', 'date': 'Wed, 26 Oct 2022 04:02:46 GMT'}, 'RetryAttempts': 0}, 'ChangeInfo': {'Id': '/change/C06780612UZZBG9OMVN6', 'Status': 'PENDING', 'SubmittedAt': datetime.datetime(2022, 10, 26, 4, 2, 47, 57000, tzinfo=tzutc())}} \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py index a3e3e573e..81c754a41 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -225,6 +225,14 @@ def test_get_manylinux_python313(self): self.assertTrue(os.path.isfile(path)) os.remove(path) + def test_manylinux_pattern_python314(self): + z = Zappa(runtime="python3.14") + wheel_filename = "psycopg_binary-3.2.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + abi3_wheel_filename = "cryptography-44.0.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + + self.assertTrue(z.manylinux_wheel_file_match.match(wheel_filename)) + self.assertTrue(z.manylinux_wheel_file_match.match(abi3_wheel_filename)) + # same, but with an ABI3 package mock_installed_packages = {"cryptography": "44.0.2"} with mock.patch( @@ -868,6 +876,49 @@ def test_update_layers(self): z.update_lambda_configuration("test", "test", "test") self.assertEqual(mock_client.update_function_configuration.call_args[1]["Layers"], []) + def test_update_capacity_provider_configuration(self): + z = Zappa(load_credentials=False) + z.credentials_arn = object() + z.lambda_client = mock.MagicMock() + z.lambda_client.get_function_configuration.return_value = {"PackageType": "Zip"} + z.wait_until_lambda_function_is_updated = mock.MagicMock() + capacity_provider_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + "PerExecutionEnvironmentMaxConcurrency": 10, + "ExecutionEnvironmentMemoryGiBPerVCpu": 4.0, + } + } + + z.update_lambda_configuration( + "test", + "test", + "test", + capacity_provider_config=capacity_provider_config, + ) + + self.assertEqual( + z.lambda_client.update_function_configuration.call_args[1]["CapacityProviderConfig"], + capacity_provider_config, + ) + + def test_update_capacity_provider_rejects_vpc(self): + z = Zappa(load_credentials=False) + z.credentials_arn = object() + z.lambda_client = mock.MagicMock() + + with self.assertRaises(ValueError): + z.update_lambda_configuration( + "arn", + "name", + "handler", + vpc_config={"SubnetIds": ["subnet-1"], "SecurityGroupIds": ["sg-1"]}, + capacity_provider_config={ + "LambdaManagedInstancesCapacityProviderConfig": {"CapacityProviderArn": "arn:aws:lambda:::capacity"} + }, + wait=False, + ) + def test_snap_start_configuration(self): """ Test that SnapStart configuration is correctly set in Lambda configuration. @@ -884,6 +935,22 @@ def test_snap_start_configuration(self): zappa_cli.load_settings("tests/test_settings.yaml") self.assertEqual("None", zappa_cli.snap_start) + def test_capacity_provider_configuration(self): + """ + Test that capacity provider configuration is loaded from settings. + """ + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "capacity_provider_enabled" + zappa_cli.load_settings("tests/test_settings.yaml") + expected_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + "PerExecutionEnvironmentMaxConcurrency": 5, + "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0, + } + } + self.assertEqual(expected_config, zappa_cli.capacity_provider_config) + def test_update_empty_aws_env_hash(self): z = Zappa() z.credentials_arn = object() @@ -3439,6 +3506,165 @@ def test_zappa_core_undeploy_lambda_alb(self): elbv2_stubber.activate() zappa_core.undeploy_lambda_alb(**kwargs) + def test_create_lambda_capacity_provider_config(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.credentials_arn = "arn:aws:iam::123:role/zappa" + zappa_core.lambda_client = mock.MagicMock() + zappa_core.wait_until_lambda_function_is_active = mock.MagicMock() + zappa_core.lambda_client.create_function.return_value = { + "FunctionArn": "abc", + "Version": 1, + } + capacity_provider_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + "PerExecutionEnvironmentMaxConcurrency": 3, + "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0, + } + } + + zappa_core.create_lambda_function( + function_name="abc", + handler="handler.lambda_handler", + docker_image_uri="123456789012.dkr.ecr.us-east-1.amazonaws.com/repo:latest", + capacity_provider_config=capacity_provider_config, + ) + + self.assertEqual( + zappa_core.lambda_client.create_function.call_args[1]["CapacityProviderConfig"], + capacity_provider_config, + ) + + def test_wait_for_capacity_provider_response(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + expected = {"FunctionVersions": []} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = expected + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + ) + + zappa_core.lambda_client.list_function_versions_by_capacity_provider.assert_called_with( + CapacityProviderName="provider", + ) + self.assertEqual(result, expected) + + def test_wait_for_capacity_provider_response_waits_until_active(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + function_arn = "arn:aws:lambda:us-east-1:123:function:test:1" + pending = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Pending"}]} + active = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Active"}]} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.side_effect = [pending, active] + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Active", + max_attempts=2, + delay_seconds=0, + ) + + self.assertEqual(result, active) + self.assertEqual(zappa_core.lambda_client.list_function_versions_by_capacity_provider.call_count, 2) + + def test_wait_for_capacity_provider_response_waits_until_empty(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + function_arn = "arn:aws:lambda:us-east-1:123:function:test:1" + present = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Pending"}]} + gone = {"FunctionVersions": []} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.side_effect = [present, gone] + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Empty", + max_attempts=2, + delay_seconds=0, + ) + + self.assertEqual(result, gone) + self.assertEqual(zappa_core.lambda_client.list_function_versions_by_capacity_provider.call_count, 2) + + def test_wait_for_capacity_provider_response_exits_on_failed(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + function_arn = "arn:aws:lambda:us-east-1:123:function:test:1" + failed = {"FunctionVersions": [{"FunctionArn": function_arn, "State": "Failed"}]} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = failed + + with self.assertRaises(RuntimeError): + zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Active", + max_attempts=1, + delay_seconds=0, + ) + + with self.assertRaises(RuntimeError): + zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + function_arn=function_arn, + function_state="Empty", + max_attempts=1, + delay_seconds=0, + ) + + def test_wait_for_capacity_provider_response_passes_marker(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + expected = {"FunctionVersions": []} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = expected + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + marker="m", + ) + + zappa_core.lambda_client.list_function_versions_by_capacity_provider.assert_called_with( + CapacityProviderName="provider", + Marker="m", + ) + self.assertEqual(result, expected) + + def test_wait_for_capacity_provider_response_retries(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.lambda_client = mock.MagicMock() + error = botocore.exceptions.ClientError( + {"Error": {"Code": "ThrottlingException", "Message": "slow down"}}, "ListFunctionVersionsByCapacityProvider" + ) + success = {"FunctionVersions": [{"FunctionVersion": "1"}]} + zappa_core.lambda_client.list_function_versions_by_capacity_provider.side_effect = [error, success] + + result = zappa_core.wait_for_capacity_provider_response( + capacity_provider_name="provider", + max_attempts=2, + delay_seconds=0, + ) + + self.assertEqual(result, success) + self.assertEqual(zappa_core.lambda_client.list_function_versions_by_capacity_provider.call_count, 2) + + def test_create_lambda_capacity_provider_rejects_vpc(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.credentials_arn = "arn:aws:iam::123:role/zappa" + zappa_core.lambda_client = mock.MagicMock() + + with self.assertRaises(ValueError): + zappa_core.create_lambda_function( + function_name="abc", + handler="handler.lambda_handler", + bucket="bucket", + s3_key="key", + capacity_provider_config={ + "LambdaManagedInstancesCapacityProviderConfig": {"CapacityProviderArn": "arn:aws:lambda:::capacity"} + }, + vpc_config={"SubnetIds": ["subnet-1"], "SecurityGroupIds": ["sg-1"]}, + ) + @mock.patch("botocore.client") def test_set_lambda_concurrency(self, client): boto_mock = mock.MagicMock() @@ -3469,6 +3695,7 @@ def test_update_lambda_concurrency(self, client): aws_region="test", load_credentials=True, ) + zappa_core.lambda_client.get_function_configuration.return_value = {} zappa_core.lambda_client.create_function.return_value = { "FunctionArn": "abc", "Version": 1, @@ -3484,6 +3711,52 @@ def test_update_lambda_concurrency(self, client): ) boto_mock.client().delete_function_concurrency.assert_not_called() + @mock.patch("botocore.client") + def test_update_lambda_concurrency_skipped_for_capacity_provider(self, client): + boto_mock = mock.MagicMock() + zappa_core = Zappa( + boto_session=boto_mock, + profile_name="test", + aws_region="test", + load_credentials=True, + ) + zappa_core.lambda_client.create_function.return_value = { + "FunctionArn": "abc", + "Version": 1, + } + zappa_core.lambda_client.get_function_configuration.return_value = {"CapacityProviderConfig": {"foo": "bar"}} + + zappa_core.update_lambda_function(bucket="test", function_name="abc") + + boto_mock.client().put_function_concurrency.assert_not_called() + boto_mock.client().delete_function_concurrency.assert_not_called() + + def test_create_lambda_capacity_provider_skips_concurrency(self): + zappa_core = Zappa(load_credentials=False) + zappa_core.credentials_arn = "arn:aws:iam::123:role/zappa" + zappa_core.lambda_client = mock.MagicMock() + zappa_core.wait_until_lambda_function_is_active = mock.MagicMock() + zappa_core.lambda_client.create_function.return_value = { + "FunctionArn": "abc", + "Version": 1, + } + capacity_provider_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + } + } + + zappa_core.create_lambda_function( + function_name="abc", + handler="handler.lambda_handler", + bucket="bucket", + s3_key="key", + capacity_provider_config=capacity_provider_config, + concurrency=5, + ) + + zappa_core.lambda_client.put_function_concurrency.assert_not_called() + @mock.patch("botocore.client") def test_delete_lambda_concurrency(self, client): boto_mock = mock.MagicMock() @@ -3493,6 +3766,7 @@ def test_delete_lambda_concurrency(self, client): aws_region="test", load_credentials=True, ) + zappa_core.lambda_client.get_function_configuration.return_value = {} zappa_core.lambda_client.create_function.return_value = { "FunctionArn": "abc", "Version": 1, @@ -4120,6 +4394,17 @@ def test_unsupported_version_error(self, *_): reload(zappa) + @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 14)) + def test_supported_python_version(self, *_): + from importlib import reload + + try: + import zappa + + reload(zappa) + except RuntimeError as exc: # pragma: no cover + self.fail(f"RuntimeError raised for supported Python version: {exc}") + @mock.patch("os.getenv", return_value="True") @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 6)) def test_minor_version_only_check_when_in_docker(self, *_): diff --git a/tests/test_settings.yaml b/tests/test_settings.yaml index 618341168..c5e91dede 100644 --- a/tests/test_settings.yaml +++ b/tests/test_settings.yaml @@ -60,3 +60,10 @@ snap_start_enabled: snap_start_disabled: extends: ttt888 snap_start: None +capacity_provider_enabled: + extends: ttt888 + capacity_provider_config: + LambdaManagedInstancesCapacityProviderConfig: + CapacityProviderArn: arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test + PerExecutionEnvironmentMaxConcurrency: 5 + ExecutionEnvironmentMemoryGiBPerVCpu: 2.0 diff --git a/tests/test_wsgi_script_name_settings.py b/tests/test_wsgi_script_name_settings.py index d2ec45cd9..4174cf73f 100644 --- a/tests/test_wsgi_script_name_settings.py +++ b/tests/test_wsgi_script_name_settings.py @@ -10,3 +10,4 @@ LOG_LEVEL = "DEBUG" PROJECT_NAME = "wsgi_script_name_settings" COGNITO_TRIGGER_MAPPING = {} +EXCEPTION_HANDLER = None diff --git a/zappa/cli.py b/zappa/cli.py index 791473887..c275133db 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -28,6 +28,8 @@ import botocore import click import hjson as json + +# import pkg_resources import requests import slugify import toml @@ -125,6 +127,7 @@ class ZappaCLI: xray_tracing = False aws_kms_key_arn = "" snap_start = None + capacity_provider_config = None context_header_mappings = None additional_text_mimetypes = None tags = [] # type: ignore[var-annotated] @@ -201,6 +204,9 @@ def handle(self, argv=None): desc = "Zappa - Deploy Python applications to AWS Lambda" " and API Gateway.\n" parser = argparse.ArgumentParser(description=desc) + from importlib.metadata import version + + zappa_version = version("zappa") parser.add_argument( "-v", "--version", @@ -591,7 +597,7 @@ def dispatch_command(self, command, stage): + click.style(self.api_stage, bold=True) + ".." ) - + click.echo(self.vargs) # Explicitly define the app function. # Related: https://github.com/Miserlou/Zappa/issues/832 if self.vargs.get("app_function", None): @@ -913,9 +919,11 @@ def deploy(self, source_zip=None, docker_image_uri=None): runtime=self.runtime, aws_environment_variables=self.aws_environment_variables, aws_kms_key_arn=self.aws_kms_key_arn, + capacity_provider_config=self.capacity_provider_config, use_alb=self.use_alb, layers=self.layers, concurrency=self.lambda_concurrency, + architecture=self.architecture, ) kwargs["function_name"] = self.lambda_name if docker_image_uri: @@ -954,7 +962,8 @@ def deploy(self, source_zip=None, docker_image_uri=None): function_name=self.lambda_arn, function_url_config=self.function_url_config, ) - self.zappa.deploy_lambda_function_url(**kwargs) + endpoint_url = self.zappa.deploy_lambda_function_url(**kwargs) + self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name) if self.use_apigateway: # Create and configure the API Gateway @@ -1008,9 +1017,8 @@ def deploy(self, source_zip=None, docker_image_uri=None): else: self.zappa.add_api_stage_to_api_key(api_key=self.api_key, api_id=api_id, stage_name=self.api_stage) - if self.stage_config.get("touch", True): - self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name) - self.touch_endpoint(endpoint_url) + if self.stage_config.get("touch", True): + self.touch_endpoint(endpoint_url) # Finally, delete the local copy our zip package if not source_zip and not docker_image_uri: @@ -1029,7 +1037,8 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): """ Repackage and update the function code. """ - + click.echo(self.stage_config) + click.echo(self.zappa.aws_region) if not source_zip and not docker_image_uri: # Make sure we're in a venv. self.check_venv() @@ -1151,6 +1160,8 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): function_name=self.lambda_name, num_revisions=self.num_retained_versions, concurrency=self.lambda_concurrency, + capacity_provider_config=self.capacity_provider_config, + architecture=self.architecture, ) if docker_image_uri: kwargs["docker_image_uri"] = docker_image_uri @@ -1196,7 +1207,9 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): aws_kms_key_arn=self.aws_kms_key_arn, layers=self.layers, snap_start=self.snap_start, + capacity_provider_config=self.capacity_provider_config, wait=False, + architecture=self.architecture, ) # Finally, delete the local copy our zip package @@ -1264,7 +1277,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): function_name=self.lambda_arn, function_url_config=self.function_url_config, ) - self.zappa.update_lambda_function_url(**kwargs) + endpoint_url = self.zappa.update_lambda_function_url(**kwargs)[:-1] else: self.zappa.delete_lambda_function_url(self.lambda_arn) @@ -1283,6 +1296,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): endpoint_url += "/" + self.base_path deployed_string = "Your updated Zappa deployment is " + click.style("live", fg="green", bold=True) + "!" + touch_url = endpoint_url if self.use_apigateway: deployed_string = deployed_string + ": " + click.style("{}".format(endpoint_url), bold=True) @@ -1292,13 +1306,11 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): if endpoint_url != api_url: deployed_string = deployed_string + " (" + api_url + ")" - - if self.stage_config.get("touch", True): - self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name) if api_url: - self.touch_endpoint(api_url) - elif endpoint_url: - self.touch_endpoint(endpoint_url) + touch_url = api_url + + if self.stage_config.get("touch", True): + self.touch_endpoint(touch_url) click.echo(deployed_string) @@ -1368,6 +1380,9 @@ def undeploy(self, no_confirm=False, remove_logs=False): if self.use_alb: self.zappa.undeploy_lambda_alb(self.lambda_name) + # if self.function_url_domains: + # self.zappa.undeploy_function_url_custom_domain(self.lambda_name) + if self.use_apigateway: if remove_logs: self.zappa.remove_api_gateway_logs(self.lambda_name) @@ -1425,6 +1440,13 @@ def schedule(self): Given a a list of functions and a schedule to execute them, setup up regular execution. """ + self.zappa.unschedule_events( + lambda_name=self.lambda_name, + lambda_arn=self.lambda_arn, + events=[], + excluded_source_services=["dynamodb", "kinesis", "sqs"], + ) + events = self.stage_config.get("events", []) if events: @@ -1448,7 +1470,6 @@ def schedule(self): "description": "Zappa Keep Warm - {}".format(self.lambda_name), } ) - if events: try: function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name) @@ -1499,7 +1520,7 @@ def schedule(self): def unschedule(self): """ - Given a a list of scheduled functions, + Given a list of scheduled functions, tear down their regular execution. """ @@ -2300,7 +2321,7 @@ def certify(self, no_confirm=True, manual=False): Register or update a domain certificate for this env. """ - if not self.domain: + if not (self.domain or self.function_url_domains): raise ClickException( "Can't certify a domain without " + click.style("domain", fg="red", bold=True) + " configured!" ) @@ -2381,19 +2402,19 @@ def certify(self, no_confirm=True, manual=False): certificate_chain = f.read() click.echo("Certifying domain " + click.style(self.domain, fg="green", bold=True) + "..") + route53 = self.stage_config.get("route53_enabled", True) - # Get cert and update domain. + # Get cert and update domain for api_gateway + if self.use_apigateway: + # Let's Encrypt + if not cert_location and not cert_arn: + from .letsencrypt import get_cert_and_update_domain - # Let's Encrypt - if not cert_location and not cert_arn: - from .letsencrypt import get_cert_and_update_domain + cert_success = get_cert_and_update_domain(self.zappa, self.lambda_name, self.api_stage, self.domain, manual) - cert_success = get_cert_and_update_domain(self.zappa, self.lambda_name, self.api_stage, self.domain, manual) + # Custom SSL / ACM + else: - # Custom SSL / ACM - else: - route53 = self.stage_config.get("route53_enabled", True) - if self.use_apigateway: if not self.zappa.get_domain_name(self.domain, route53=route53): dns_name = self.zappa.create_domain_name( domain_name=self.domain, @@ -2428,28 +2449,29 @@ def certify(self, no_confirm=True, manual=False): ) cert_success = True + if cert_success: + click.echo("Certificate " + click.style("updated", fg="green", bold=True) + "!") + else: + click.echo(click.style("Failed", fg="red", bold=True) + " to generate or install certificate! :(") + click.echo("\n==============\n") + shamelessly_promote() - if self.use_function_url: - self.lambda_arn = self.zappa.get_lambda_function(function_name=self.lambda_name) - dns_name = self.zappa.update_lambda_function_url_domains( - self.lambda_arn, self.function_url_domains, cert_arn, self.function_url_cloudfront_config - ) - if route53: - for domain in self.function_url_domains: - self.zappa.update_route53_records(domain, dns_name) - print( - "Created a new domain name with supplied certificate. " - "Please note that it can take up to 40 minutes for this domain to be " - "created and propagated through AWS, but it requires no further work on your part." - ) - cert_success = True - - if cert_success: - click.echo("Certificate " + click.style("updated", fg="green", bold=True) + "!") - else: - click.echo(click.style("Failed", fg="red", bold=True) + " to generate or install certificate! :(") - click.echo("\n==============\n") - shamelessly_promote() + if self.use_function_url: + self.lambda_arn = self.zappa.get_lambda_function(function_name=self.lambda_name) + dns_name = self.zappa.update_lambda_function_url_domains( + self.lambda_arn, + self.function_url_domains, + cert_arn, + self.function_url_cloudfront_config, + ) + if route53: + for domain in self.function_url_domains: + self.zappa.update_route53_records(domain, dns_name) + print( + "Created a new domain name with supplied certificate. " + "Please note that it can take up to 40 minutes for this domain to be " + "created and propagated through AWS, but it requires no further work on your part." + ) ## # Shell @@ -2679,6 +2701,7 @@ def load_settings(self, settings_file=None, session=None): self.runtime = self.stage_config.get("runtime", get_runtime_from_python_version()) self.aws_kms_key_arn = self.stage_config.get("aws_kms_key_arn", "") self.snap_start = self.stage_config.get("snap_start", "None") + self.capacity_provider_config = self.stage_config.get("capacity_provider_config", None) self.context_header_mappings = self.stage_config.get("context_header_mappings", {}) self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") @@ -2715,6 +2738,9 @@ def load_settings(self, settings_file=None, session=None): # Additional tags self.tags = self.stage_config.get("tags", {}) + # Architectures + self.architecture = self.stage_config.get("architecture", "x86_64") + desired_role_name = self.lambda_name + "-ZappaLambdaExecutionRole" self.zappa = Zappa( boto_session=session, @@ -3432,6 +3458,9 @@ def touch_endpoint(self, endpoint_url): + " response code." ) + if req.status_code == 200: + click.echo(req.text) + #################################################################### # Main diff --git a/zappa/core.py b/zappa/core.py index d9c140489..a0363810d 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2,6 +2,7 @@ Zappa core library. You may also want to look at `cli.py` and `util.py`. """ +import datetime import getpass import hashlib import json @@ -16,6 +17,7 @@ import tarfile import tempfile import time +import urllib import uuid import zipfile from builtins import bytes, int @@ -162,7 +164,7 @@ def build_manylinux_wheel_file_match_pattern(runtime: str, architecture: str) -> # Support PEP600 (https://peps.python.org/pep-0600/) # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl runtime_major_version, runtime_minor_version = runtime[6:].split(".") - python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.13 -> cp313 + python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.14 -> cp314 manylinux_legacy_tags = ("manylinux2014", "manylinux2010", "manylinux1") if architecture == X86_ARCHITECTURE: valid_platform_tags = [X86_ARCHITECTURE] @@ -245,7 +247,7 @@ def __init__( load_credentials=True, desired_role_name=None, desired_role_arn=None, - runtime="python3.13", # Detected at runtime in CLI + runtime="python3.14", # Detected at runtime in CLI tags=(), endpoint_urls={}, xray_tracing=False, @@ -320,6 +322,7 @@ def __init__( self.cognito_client = self.boto_client("cognito-idp") self.sts_client = self.boto_client("sts") self.efs_client = self.boto_client("efs") + self.cloudfront_client = self.boto_client("cloudfront") self.tags = tags self.cf_template = troposphere.Template() @@ -1236,6 +1239,14 @@ def create_lambda_function( if not layers: layers = [] + uses_capacity_provider = bool(capacity_provider_config) + uses_vpc = bool(vpc_config and (vpc_config.get("SubnetIds") or vpc_config.get("SecurityGroupIds"))) + if uses_capacity_provider and uses_vpc: + raise ValueError( + "Lambda capacity providers cannot be used with VPC configurations. " + "Remove VPC settings or disable the capacity provider." + ) + kwargs = dict( FunctionName=function_name, Role=self.credentials_arn, @@ -1255,6 +1266,9 @@ def create_lambda_function( # zappa currently only supports a single architecture, and uses a str value internally Architectures=[self.architecture], ) + if capacity_provider_config: + kwargs["CapacityProviderConfig"] = capacity_provider_config + kwargs.pop("VpcConfig") if not docker_image_uri: kwargs["Runtime"] = runtime kwargs["Handler"] = handler @@ -1291,7 +1305,12 @@ def create_lambda_function( if self.tags: self.lambda_client.tag_resource(Resource=resource_arn, Tags=self.tags) - if concurrency is not None: + if uses_capacity_provider: + if concurrency is not None: + logger.warning( + "Reserved concurrency is not supported with Lambda capacity providers; skipping reserved concurrency." + ) + elif concurrency is not None: self.lambda_client.put_function_concurrency( FunctionName=resource_arn, ReservedConcurrentExecutions=concurrency, @@ -1311,7 +1330,9 @@ def update_lambda_function( local_zip=None, num_revisions=None, concurrency=None, + capacity_provider_config=None, docker_image_uri=None, + architecture=None, ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, @@ -1320,7 +1341,7 @@ def update_lambda_function( """ logger.info("Updating Lambda function code..") - kwargs = dict(FunctionName=function_name, Publish=publish) + kwargs = dict(FunctionName=function_name, Publish=publish, Architectures=[architecture]) if docker_image_uri: kwargs["ImageUri"] = docker_image_uri elif local_zip: @@ -1357,28 +1378,36 @@ def update_lambda_function( Name=ALB_LAMBDA_ALIAS, ) - if concurrency is not None: - self.lambda_client.put_function_concurrency( - FunctionName=function_name, - ReservedConcurrentExecutions=concurrency, - ) + uses_capacity_provider = bool(capacity_provider_config) + if not uses_capacity_provider: + try: + function_configuration = self.lambda_client.get_function_configuration(FunctionName=function_name) + uses_capacity_provider = bool(function_configuration.get("CapacityProviderConfig")) + except botocore.exceptions.ClientError: + uses_capacity_provider = False + + if uses_capacity_provider: + if concurrency is not None: + logger.warning( + "Reserved concurrency is not supported with Lambda capacity providers; skipping reserved concurrency." + ) else: - self.lambda_client.delete_function_concurrency(FunctionName=function_name) + if concurrency is not None: + self.lambda_client.put_function_concurrency( + FunctionName=function_name, + ReservedConcurrentExecutions=concurrency, + ) + else: + self.lambda_client.delete_function_concurrency(FunctionName=function_name) if num_revisions: # Find the existing revision IDs for the given function # Related: https://github.com/Miserlou/Zappa/issues/1402 - versions_in_lambda = [] - versions = self.lambda_client.list_versions_by_function(FunctionName=function_name) - for version in versions["Versions"]: - versions_in_lambda.append(version["Version"]) - while "NextMarker" in versions: - versions = self.lambda_client.list_versions_by_function( - FunctionName=function_name, Marker=versions["NextMarker"] - ) - for version in versions["Versions"]: - versions_in_lambda.append(version["Version"]) + versions_in_lambda = self.list_lambda_function_versions(function_name) versions_in_lambda.remove("$LATEST") + + if "$LATEST.PUBLISHED" in versions_in_lambda: + versions_in_lambda.remove("$LATEST.PUBLISHED") # Delete older revisions if their number exceeds the specified limit for version in versions_in_lambda[::-1][num_revisions:]: self.lambda_client.delete_function(FunctionName=function_name, Qualifier=version) @@ -1387,6 +1416,19 @@ def update_lambda_function( return resource_arn + def list_lambda_function_versions(self, function_name): + versions_in_lambda = [] + versions = self.lambda_client.list_versions_by_function(FunctionName=function_name) + for version in versions["Versions"]: + versions_in_lambda.append(version["Version"]) + while "NextMarker" in versions: + versions = self.lambda_client.list_versions_by_function( + FunctionName=function_name, Marker=versions["NextMarker"] + ) + for version in versions["Versions"]: + versions_in_lambda.append(version["Version"]) + return versions_in_lambda + def update_lambda_configuration( self, lambda_arn, @@ -1404,11 +1446,14 @@ def update_lambda_configuration( aws_kms_key_arn=None, layers=None, snap_start=None, + capacity_provider_config=None, wait=True, + architecture=None, ): """ Given an existing function ARN, update the configuration variables. """ + logger.info("Updating Lambda function configuration..") if not vpc_config: @@ -1424,6 +1469,14 @@ def update_lambda_configuration( if not layers: layers = [] + uses_capacity_provider = bool(capacity_provider_config) + uses_vpc = bool(vpc_config and (vpc_config.get("SubnetIds") or vpc_config.get("SecurityGroupIds"))) + if uses_capacity_provider and uses_vpc: + raise ValueError( + "Lambda capacity providers cannot be used with VPC configurations. " + "Remove VPC settings or disable the capacity provider." + ) + if wait: # Wait until function is ready, otherwise expected keys will be missing from 'lambda_aws_config'. self.wait_until_lambda_function_is_updated(function_name) @@ -1453,6 +1506,10 @@ def update_lambda_configuration( "SnapStart": {"ApplyOn": snap_start if snap_start else "None"}, } + if capacity_provider_config: + kwargs["CapacityProviderConfig"] = capacity_provider_config + kwargs.pop("VpcConfig") + if lambda_aws_config.get("PackageType", None) != "Image": kwargs.update( { @@ -1465,6 +1522,37 @@ def update_lambda_configuration( response = self.lambda_client.update_function_configuration(**kwargs) resource_arn = response["FunctionArn"] + if capacity_provider_config: + + capacity_provider_arn = capacity_provider_config["LambdaManagedInstancesCapacityProviderConfig"][ + "CapacityProviderArn" + ] + capacity_provider_name_part = capacity_provider_arn + if "capacity-provider/" in capacity_provider_name_part: + capacity_provider_name_part = capacity_provider_name_part.split("capacity-provider/", 1)[1] + elif "capacity-provider:" in capacity_provider_name_part: + capacity_provider_name_part = capacity_provider_name_part.split("capacity-provider:", 1)[1] + capacity_provider_name = capacity_provider_name_part.rsplit("/", 1)[-1] + # wait for latest version + versions_in_lambda = self.list_lambda_function_versions(function_name=function_name) + latest_version = max(int(v) for v in versions_in_lambda if v.isdigit()) + + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=f"{response["FunctionArn"]}:{latest_version}", + function_state="Active", + ) + + # publish to latest + response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo='LATEST_PUBLISHED') + logger.info(f"Publish to {response['FunctionArn']}") + + time.sleep(10) + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=response["FunctionArn"], + function_state="Active", + ) if self.tags: self.lambda_client.tag_resource(Resource=resource_arn, Tags=self.tags) @@ -1553,6 +1641,94 @@ def wait_until_lambda_function_is_updated(self, function_name): logger.info(f"Waiting for lambda function [{function_name}] to be updated...") waiter.wait(FunctionName=function_name) + def wait_for_capacity_provider_response( + self, + function_arn: str, + capacity_provider_name: str, + function_state: str = "Active", + marker: str | None = None, + max_attempts: int = 60, + delay_seconds: float = 5, + ): + """ + Call list_function_versions_by_capacity_provider with retry. + + If function_arn is provided: + - function_state="Active": poll until the matching FunctionVersion has State=Active + - function_state="Empty": poll until the matching FunctionVersion record is no longer returned + In both modes, if the matching record is ever seen in State=Failed, raises RuntimeError. + Returns the last API response seen. + """ + if not capacity_provider_name: + raise ValueError("capacity_provider_name is required.") + + normalized_state = (function_state or "").strip().lower() + if normalized_state not in ("active", "empty"): + raise ValueError("function_state must be 'Active' or 'Empty'.") + + list_kwargs = {"CapacityProviderName": capacity_provider_name} + if marker: + list_kwargs["Marker"] = marker + + + logger.info(f"Wait for {function_arn} to be {function_state} ...") + last_response = None + for attempt in range(1, max_attempts + 1): + try: + response = self.lambda_client.list_function_versions_by_capacity_provider(**list_kwargs) + last_response = response + except botocore.exceptions.ClientError as e: + error_code = e.response.get("Error", {}).get("Code") + if error_code in ("ThrottlingException", "Throttling", "TooManyRequestsException") and attempt < max_attempts: + time.sleep(delay_seconds) + continue + raise + + if not function_arn: + return response + + matched_item = None + page = response or {} + while True: + for item in (page.get("FunctionVersions") or []): + arn = item.get("FunctionArn") or "" + if function_arn in arn: + matched_item = item + break + if matched_item: + break + + next_marker = page.get("NextMarker") + if not next_marker: + break + + page = self.lambda_client.list_function_versions_by_capacity_provider( + CapacityProviderName=capacity_provider_name, + Marker=next_marker, + ) + last_response = page + + if matched_item: + state = matched_item.get("State") + logger.info(f"{attempt}/{max_attempts} attempts: current state {state}, expect {function_state}") + if state == "Failed": + raise RuntimeError( + f"Function version [{matched_item.get('FunctionArn')}] entered Failed state under " + f"capacity provider [{capacity_provider_name}]." + ) + if normalized_state == "active" and state == 'Active': + return last_response + else: + if normalized_state == "empty": + return last_response + + time.sleep(delay_seconds) + + raise TimeoutError( + f"Timed out waiting for function [{function_arn}] to become {function_state} under capacity provider " + f"[{capacity_provider_name}] after {max_attempts} attempts." + ) + def get_lambda_function(self, function_name): """ Returns the lambda function ARN, given a name @@ -1649,7 +1825,7 @@ def deploy_lambda_function_url(self, function_name, function_url_config): response = self.lambda_client.create_function_url_config( FunctionName=function_name, AuthType=function_url_config["authorizer"] ) - print("function URL address: {}".format(response["FunctionUrl"])) + logger.info("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(function_name, function_url_config) return response @@ -1672,19 +1848,21 @@ def update_lambda_function_url(self, function_name, function_url_config): ) else: response = self.lambda_client.update_function_url_config( - FunctionName=function_name, AuthType=function_url_config["authorizer"] + FunctionName=function_name, + AuthType=function_url_config["authorizer"], ) - print("function URL address: {}".format(response["FunctionUrl"])) + logger.info("function URL address: {}".format(response["FunctionUrl"])) self.update_function_url_policy(config["FunctionArn"], function_url_config) + return response["FunctionUrl"] else: - self.deploy_lambda_function_url(function_name, function_url_config) + return self.deploy_lambda_function_url(function_name, function_url_config) def delete_lambda_function_url(self, function_name): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) for config in response.get("FunctionUrlConfigs", []): resp = self.lambda_client.delete_function_url_config(FunctionName=config["FunctionArn"]) if resp["ResponseMetadata"]["HTTPStatusCode"] == 204: - print("function URL deleted: {}".format(config["FunctionUrl"])) + logger.info("function URL deleted: {}".format(config["FunctionUrl"])) self.delete_function_url_policy(config["FunctionArn"]) ## @@ -1693,7 +1871,7 @@ def delete_lambda_function_url(self, function_name): def update_lambda_function_url_domains(self, function_name, function_url_domains, certificate_arn, cloudfront_config): response = self.lambda_client.list_function_url_configs(FunctionName=function_name, MaxItems=50) if not response.get("FunctionUrlConfigs", []): - print("no function url configured on lambda, skip setting custom domains") + logger.info("no function url configured on lambda, skip setting custom domains") url = response["FunctionUrlConfigs"][0]["FunctionUrl"] import urllib @@ -1772,7 +1950,7 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains if not distributions: response = self.cloudfront_client.create_distribution(DistributionConfig=config) if response["ResponseMetadata"]["HTTPStatusCode"] == 201: - print( + logger.info( "created cloudfront distribution for {}. It will take a while for the change to be deployed.".format( function_url_domains ) @@ -1782,13 +1960,15 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains id = distributions[0]["Id"] distribution = self.cloudfront_client.get_distribution(Id=id) new_config = distribution["Distribution"]["DistributionConfig"] - new_config.update(config) + updates = config.copy() + updates.pop("CallerReference") + new_config.update(updates) response = self.cloudfront_client.update_distribution( DistributionConfig=new_config, Id=id, IfMatch=distribution["ETag"] ) if response["ResponseMetadata"]["HTTPStatusCode"] == 200: - print( + logger.info( "update cloudfront distribution for {}. It will take a while for the change to be deployed.".format( function_url_domains ) @@ -2547,6 +2727,41 @@ def get_rest_apis(self, project_name): continue yield api + def undeploy_function_url_custom_domain(self, lambda_name, domains=None): + + response = self.lambda_client.list_function_url_configs(FunctionName=lambda_name, MaxItems=50) + if not response.get("FunctionUrlConfigs", []): + logger.info("no function url configured on lambda. skip delete custom domains") + url = response["FunctionUrlConfigs"][0]["FunctionUrl"] + url = urllib.parse.urlparse(url) + distributions = self.cloudfront_client.list_distributions() + distributions = [ + item + for item in distributions["DistributionList"]["Items"] + if url.hostname in [origin["DomainName"] for origin in item["Origins"]["Items"]] + ] + + for distribution in distributions: + id = distribution["Id"] + distribution = self.cloudfront_client.get_distribution(Id=id) + new_config = distribution["Distribution"]["DistributionConfig"] + new_config["Enabled"] = False + + response = self.cloudfront_client.update_distribution( + DistributionConfig=new_config, Id=id, IfMatch=distribution["ETag"] + ) + response + logger.info("wait for distribution to be disabled.") + waiter = self.cloudfront_client.get_waiter("distribution_deployed") + waiter.wait(Id=id, WaiterConfig={"Delay": 20, "MaxAttempts": 20}) + delete_response = self.cloudfront_client.delete_distribution(Id=id, IfMatch=response["ETag"]) + if delete_response["ResponseMetadata"]["HTTPStatusCode"] == 204: + logger.info("cloudfront distribution deleted") + + # attempt remove 53 record + for domain in distribution["Distribution"]["DistributionConfig"]["Aliases"].get("Items", []): + self.delete_route53_records(domain, distribution["Distribution"]["DomainName"]) + def undeploy_api_gateway(self, lambda_name, domain_name=None, base_path=None): """ Delete a deployed REST API Gateway. @@ -2879,7 +3094,8 @@ def get_api_id(self, lambda_name: str, apigateway_version: str = DEFAULT_APIGATE if item["name"] == lambda_name: return item["id"] - logger.exception("Could not get API ID.") + logger.exception(f"Could not get API ID. {lambda_name} {self.boto_session.region_name}") + logger.exception(response) return None except Exception: # pragma: no cover # We don't even have an API deployed. That's okay! @@ -2917,17 +3133,19 @@ def create_domain_name( certificateName=certificate_name, certificateArn=certificate_arn, ) + api_id = self.get_api_id(lambda_name) + if not api_id: + raise LookupError("No API URL to certify found - did you deploy?") - api_id = self.get_api_id(lambda_name) - if not api_id: - raise LookupError("No API URL to certify found - did you deploy?") + self.apigateway_client.create_base_path_mapping( + domainName=domain_name, + basePath="" if base_path is None else base_path, + restApiId=api_id, + stage=stage, + ) - self.apigateway_client.create_base_path_mapping( - domainName=domain_name, - basePath="" if base_path is None else base_path, - restApiId=api_id, - stage=stage, - ) + if self.function_url_enabled: + pass return agw_response["distributionDomainName"] @@ -2959,11 +3177,7 @@ def update_route53_records(self, domain_name, dns_name): # Related: https://github.com/boto/boto3/issues/157 # and: http://docs.aws.amazon.com/Route53/latest/APIReference/CreateAliasRRSAPI.html # and policy: https://spin.atomicobject.com/2016/04/28/route-53-hosted-zone-managment/ - # pure_zone_id = zone_id.split('/hostedzone/')[1] - # XXX: ClientError: An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: - # Tried to create an alias that targets d1awfeji80d0k2.cloudfront.net., type A in zone Z1XWOQP59BYF6Z, - # but the alias target name does not lie within the target zone response = self.route53.change_resource_record_sets( HostedZoneId=zone_id, ChangeBatch={"Changes": [{"Action": "UPSERT", "ResourceRecordSet": record_set}]}, @@ -2971,6 +3185,41 @@ def update_route53_records(self, domain_name, dns_name): return response + def delete_route53_records(self, domain_name, dns_name): + """ + Updates Route53 Records following GW domain creation + """ + zone_id = self.get_hosted_zone_id_for_domain(domain_name) + + is_apex = self.route53.get_hosted_zone(Id=zone_id)["HostedZone"]["Name"][:-1] == domain_name + if is_apex: + record_set = { + "Name": domain_name, + "Type": "A", + "AliasTarget": { + "HostedZoneId": "Z2FDTNDATAQYW2", # This is a magic value that means "CloudFront" + "DNSName": dns_name, + "EvaluateTargetHealth": False, + }, + } + else: + record_set = { + "Name": domain_name, + "Type": "CNAME", + "ResourceRecords": [{"Value": dns_name}], + "TTL": 60, + } + + response = self.route53.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={"Changes": [{"Action": "DELETE", "ResourceRecordSet": record_set}]}, + ) + + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + logger.info("removed route 53 record for {}".format(domain_name)) + + return response + def update_domain_name( self, domain_name, @@ -2983,6 +3232,8 @@ def update_domain_name( stage=None, route53=True, base_path=None, + use_apigateway=True, + use_function_url=False, ): """ This updates your certificate information for an existing domain, @@ -3009,19 +3260,27 @@ def update_domain_name( ) certificate_arn = acm_certificate["CertificateArn"] - self.update_domain_base_path_mapping(domain_name, lambda_name, stage, base_path) + if use_apigateway: + self.update_domain_base_path_mapping(domain_name, lambda_name, stage, base_path) - return self.apigateway_client.update_domain_name( - domainName=domain_name, - patchOperations=[ - { - "op": "replace", - "path": "/certificateName", - "value": certificate_name, - }, - {"op": "replace", "path": "/certificateArn", "value": certificate_arn}, - ], - ) + res = self.apigateway_client.update_domain_name( + domainName=domain_name, + patchOperations=[ + { + "op": "replace", + "path": "/certificateName", + "value": certificate_name, + }, + { + "op": "replace", + "path": "/certificateArn", + "value": certificate_arn, + }, + ], + ) + if use_function_url: + pass + return res def update_domain_base_path_mapping(self, domain_name, lambda_name, stage, base_path): """ @@ -3169,12 +3428,11 @@ def _clear_policy(self, lambda_name): if policy_response["ResponseMetadata"]["HTTPStatusCode"] == 200: statement = json.loads(policy_response["Policy"])["Statement"] for s in statement: - # Only remove CloudWatch Events permissions (created by schedule_events) - principal = s.get("Principal", {}) - if isinstance(principal, dict) and principal.get("Service") == "events.amazonaws.com": - delete_response = self.lambda_client.remove_permission(FunctionName=lambda_name, StatementId=s["Sid"]) - if delete_response["ResponseMetadata"]["HTTPStatusCode"] != 204: - logger.error("Failed to delete an obsolete policy statement: {}".format(policy_response)) + if s["Sid"] in ["FunctionURLAllowPublicAccess"]: + continue + delete_response = self.lambda_client.remove_permission(FunctionName=lambda_name, StatementId=s["Sid"]) + if delete_response["ResponseMetadata"]["HTTPStatusCode"] != 204: + logger.error("Failed to delete an obsolete policy statement: {}".format(policy_response)) else: logger.debug("Failed to load Lambda function policy: {}".format(policy_response)) except ClientError as e: @@ -3198,7 +3456,7 @@ def create_event_permission(self, lambda_name, principal, source_arn): permission_response = self.lambda_client.add_permission( FunctionName=lambda_name, - StatementId="".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)), + StatementId="zappa-" + "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)), Action="lambda:InvokeFunction", Principal=principal, SourceArn=source_arn, @@ -3229,18 +3487,17 @@ def schedule_events(self, lambda_arn, lambda_name, events, default=True): # and do not require event permissions. They do require additional permissions on the Lambda roles though. # http://docs.aws.amazon.com/lambda/latest/dg/lambda-api-permissions-ref.html pull_services = ["dynamodb", "kinesis", "sqs"] - - # XXX: Not available in Lambda yet. - # We probably want to execute the latest code. - # if default: - # lambda_arn = lambda_arn + ":$LATEST" - self.unschedule_events( lambda_name=lambda_name, lambda_arn=lambda_arn, events=events, excluded_source_services=pull_services, ) + # XXX: Not available in Lambda yet. + # We probably want to execute the latest code. + # if default: + # lambda_arn = lambda_arn + ":$LATEST" + for event in events: function = event["function"] expression = event.get("expression", None) # single expression @@ -3741,11 +3998,10 @@ def load_credentials(self, boto_session=None, profile_name=None): if profile_name: self.boto_session = boto3.Session(profile_name=profile_name, region_name=self.aws_region) elif os.environ.get("AWS_ACCESS_KEY_ID") and os.environ.get("AWS_SECRET_ACCESS_KEY"): - region_name = os.environ.get("AWS_DEFAULT_REGION") or self.aws_region session_kw = { "aws_access_key_id": os.environ.get("AWS_ACCESS_KEY_ID"), "aws_secret_access_key": os.environ.get("AWS_SECRET_ACCESS_KEY"), - "region_name": region_name, + "region_name": self.aws_region, } # If we're executing in a role, AWS_SESSION_TOKEN will be present, too. diff --git a/zappa/handler.py b/zappa/handler.py index 19d61c967..03fca23a3 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -526,6 +526,19 @@ def handler(self, event, context): try: time_start = datetime.datetime.now() + script_name = "" + host = event.get("headers", {}).get("host") + if host: + if "amazonaws.com" in host: + logger.debug("amazonaws found in host") + # The path provided in th event doesn't include the + # stage, so we must tell Flask to include the API + # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 + script_name = f"/{settings.API_STAGE}" + # fix function url domain + if host.find("lambda-url") > -1 and event.get("headers", {}).get("cloudfront-host"): + # https://stackoverflow.com/questions/73024633/cloudfront-forward-host-header-to-lambda-function-url-origin + event["headers"]["host"] = event["headers"]["cloudfront-host"] # Determine if this is API Gateway v2 (has stage) or Function URL (no stage) # API Gateway v2 includes stage in requestContext request_context = event.get("requestContext", {}) @@ -537,7 +550,7 @@ def handler(self, event, context): if stage: # API Gateway v2 with named stage - rawPath includes the stage script_name = f"/{stage}" - else: + if host.find("lambda-url") > -1: # Function URL - no stage script_name = "" From 07cf07277235cbb0ec327c4c9ae6bf6d41dfb3c4 Mon Sep 17 00:00:00 2001 From: versionbump Date: Fri, 23 Jan 2026 13:07:15 +0800 Subject: [PATCH 098/111] Update core.py --- zappa/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zappa/core.py b/zappa/core.py index a0363810d..ef8f09fe9 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1213,6 +1213,7 @@ def create_lambda_function( aws_environment_variables=None, aws_kms_key_arn=None, snap_start=None, + capacity_provider_config=None, xray_tracing=False, local_zip=None, use_alb=False, From 6aeeee2492fe88e17509e16af7b10197960ba903 Mon Sep 17 00:00:00 2001 From: versionbump Date: Fri, 23 Jan 2026 13:20:59 +0800 Subject: [PATCH 099/111] Update core.py --- zappa/core.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index ef8f09fe9..a48a2acb1 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2,7 +2,6 @@ Zappa core library. You may also want to look at `cli.py` and `util.py`. """ -import datetime import getpass import hashlib import json @@ -1423,9 +1422,7 @@ def list_lambda_function_versions(self, function_name): for version in versions["Versions"]: versions_in_lambda.append(version["Version"]) while "NextMarker" in versions: - versions = self.lambda_client.list_versions_by_function( - FunctionName=function_name, Marker=versions["NextMarker"] - ) + versions = self.lambda_client.list_versions_by_function(FunctionName=function_name, Marker=versions["NextMarker"]) for version in versions["Versions"]: versions_in_lambda.append(version["Version"]) return versions_in_lambda @@ -1543,9 +1540,9 @@ def update_lambda_configuration( function_arn=f"{response["FunctionArn"]}:{latest_version}", function_state="Active", ) - + # publish to latest - response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo='LATEST_PUBLISHED') + response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo="LATEST_PUBLISHED") logger.info(f"Publish to {response['FunctionArn']}") time.sleep(10) @@ -1671,7 +1668,6 @@ def wait_for_capacity_provider_response( if marker: list_kwargs["Marker"] = marker - logger.info(f"Wait for {function_arn} to be {function_state} ...") last_response = None for attempt in range(1, max_attempts + 1): @@ -1691,7 +1687,7 @@ def wait_for_capacity_provider_response( matched_item = None page = response or {} while True: - for item in (page.get("FunctionVersions") or []): + for item in page.get("FunctionVersions") or []: arn = item.get("FunctionArn") or "" if function_arn in arn: matched_item = item @@ -1717,7 +1713,7 @@ def wait_for_capacity_provider_response( f"Function version [{matched_item.get('FunctionArn')}] entered Failed state under " f"capacity provider [{capacity_provider_name}]." ) - if normalized_state == "active" and state == 'Active': + if normalized_state == "active" and state == "Active": return last_response else: if normalized_state == "empty": From abbbafbbb9aa537cb9a5be9e08ff6b8322acbebb Mon Sep 17 00:00:00 2001 From: versionbump Date: Fri, 23 Jan 2026 14:26:17 +0800 Subject: [PATCH 100/111] wip --- zappa/cli.py | 5 ----- zappa/core.py | 27 +++++++++++++++------------ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index c275133db..8c4cf69d0 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -204,9 +204,6 @@ def handle(self, argv=None): desc = "Zappa - Deploy Python applications to AWS Lambda" " and API Gateway.\n" parser = argparse.ArgumentParser(description=desc) - from importlib.metadata import version - - zappa_version = version("zappa") parser.add_argument( "-v", "--version", @@ -1380,8 +1377,6 @@ def undeploy(self, no_confirm=False, remove_logs=False): if self.use_alb: self.zappa.undeploy_lambda_alb(self.lambda_name) - # if self.function_url_domains: - # self.zappa.undeploy_function_url_custom_domain(self.lambda_name) if self.use_apigateway: if remove_logs: diff --git a/zappa/core.py b/zappa/core.py index a48a2acb1..6698624a7 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1332,7 +1332,6 @@ def update_lambda_function( concurrency=None, capacity_provider_config=None, docker_image_uri=None, - architecture=None, ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, @@ -1341,7 +1340,7 @@ def update_lambda_function( """ logger.info("Updating Lambda function code..") - kwargs = dict(FunctionName=function_name, Publish=publish, Architectures=[architecture]) + kwargs = dict(FunctionName=function_name, Publish=publish, Architectures=[self.architecture]) if docker_image_uri: kwargs["ImageUri"] = docker_image_uri elif local_zip: @@ -1349,6 +1348,7 @@ def update_lambda_function( else: kwargs["S3Bucket"] = bucket kwargs["S3Key"] = s3_key + response = self.lambda_client.update_function_code(**kwargs) resource_arn = response["FunctionArn"] @@ -1445,8 +1445,7 @@ def update_lambda_configuration( layers=None, snap_start=None, capacity_provider_config=None, - wait=True, - architecture=None, + wait=True ): """ Given an existing function ARN, update the configuration variables. @@ -1531,15 +1530,19 @@ def update_lambda_configuration( elif "capacity-provider:" in capacity_provider_name_part: capacity_provider_name_part = capacity_provider_name_part.split("capacity-provider:", 1)[1] capacity_provider_name = capacity_provider_name_part.rsplit("/", 1)[-1] - # wait for latest version + versions_in_lambda = self.list_lambda_function_versions(function_name=function_name) - latest_version = max(int(v) for v in versions_in_lambda if v.isdigit()) - - self.wait_for_capacity_provider_response( - capacity_provider_name=capacity_provider_name, - function_arn=f"{response["FunctionArn"]}:{latest_version}", - function_state="Active", - ) + + if versions_in_lambda: + # wait for latest version + latest_version = max(int(v) for v in versions_in_lambda if v.isdigit()) + + if latest_version: + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=f"{response["FunctionArn"]}:{latest_version}", + function_state="Active", + ) # publish to latest response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo="LATEST_PUBLISHED") From bf71a401b3ecd72a942a5746b519111e0cb9b477 Mon Sep 17 00:00:00 2001 From: versionbump Date: Fri, 23 Jan 2026 14:52:11 +0800 Subject: [PATCH 101/111] wip --- tests/test_core.py | 3 +++ zappa/cli.py | 3 +-- zappa/core.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 81c754a41..d030aa043 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3542,6 +3542,7 @@ def test_wait_for_capacity_provider_response(self): zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = expected result = zappa_core.wait_for_capacity_provider_response( + function_arn=None, capacity_provider_name="provider", ) @@ -3620,6 +3621,7 @@ def test_wait_for_capacity_provider_response_passes_marker(self): zappa_core.lambda_client.list_function_versions_by_capacity_provider.return_value = expected result = zappa_core.wait_for_capacity_provider_response( + function_arn=None, capacity_provider_name="provider", marker="m", ) @@ -3640,6 +3642,7 @@ def test_wait_for_capacity_provider_response_retries(self): zappa_core.lambda_client.list_function_versions_by_capacity_provider.side_effect = [error, success] result = zappa_core.wait_for_capacity_provider_response( + function_arn=None, capacity_provider_name="provider", max_attempts=2, delay_seconds=0, diff --git a/zappa/cli.py b/zappa/cli.py index 8c4cf69d0..814fcad41 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1157,8 +1157,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): function_name=self.lambda_name, num_revisions=self.num_retained_versions, concurrency=self.lambda_concurrency, - capacity_provider_config=self.capacity_provider_config, - architecture=self.architecture, + capacity_provider_config=self.capacity_provider_config ) if docker_image_uri: kwargs["docker_image_uri"] = docker_image_uri diff --git a/zappa/core.py b/zappa/core.py index 6698624a7..76dc4b9c5 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -3998,10 +3998,11 @@ def load_credentials(self, boto_session=None, profile_name=None): if profile_name: self.boto_session = boto3.Session(profile_name=profile_name, region_name=self.aws_region) elif os.environ.get("AWS_ACCESS_KEY_ID") and os.environ.get("AWS_SECRET_ACCESS_KEY"): + region_name = os.environ.get("AWS_DEFAULT_REGION") or self.aws_region session_kw = { "aws_access_key_id": os.environ.get("AWS_ACCESS_KEY_ID"), "aws_secret_access_key": os.environ.get("AWS_SECRET_ACCESS_KEY"), - "region_name": self.aws_region, + "region_name": region_name, } # If we're executing in a role, AWS_SESSION_TOKEN will be present, too. From f4667516fe83437a3c4c1ee54976ec336ef679f2 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 26 Jan 2026 14:33:00 +0800 Subject: [PATCH 102/111] fix tests --- tests/test_core.py | 13 ++++++++++++- zappa/cli.py | 4 +--- zappa/core.py | 7 +++---- zappa/handler.py | 15 +-------------- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index d030aa043..a0de6d64d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -881,10 +881,21 @@ def test_update_capacity_provider_configuration(self): z.credentials_arn = object() z.lambda_client = mock.MagicMock() z.lambda_client.get_function_configuration.return_value = {"PackageType": "Zip"} + capacity_provider_arn = "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test" + z.lambda_client.list_function_versions_by_capacity_provider.return_value = { + "CapacityProviderArn": capacity_provider_arn, + "FunctionVersions": [ + {"FunctionArn": "test", "State": "Active"}, + ], + "NextMarker": "", + } + z.lambda_client.publish_version.return_value = { + "FunctionArn": "test", + } z.wait_until_lambda_function_is_updated = mock.MagicMock() capacity_provider_config = { "LambdaManagedInstancesCapacityProviderConfig": { - "CapacityProviderArn": "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test", + "CapacityProviderArn": capacity_provider_arn, "PerExecutionEnvironmentMaxConcurrency": 10, "ExecutionEnvironmentMemoryGiBPerVCpu": 4.0, } diff --git a/zappa/cli.py b/zappa/cli.py index 814fcad41..425e07c28 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1157,7 +1157,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): function_name=self.lambda_name, num_revisions=self.num_retained_versions, concurrency=self.lambda_concurrency, - capacity_provider_config=self.capacity_provider_config + capacity_provider_config=self.capacity_provider_config, ) if docker_image_uri: kwargs["docker_image_uri"] = docker_image_uri @@ -1205,7 +1205,6 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): snap_start=self.snap_start, capacity_provider_config=self.capacity_provider_config, wait=False, - architecture=self.architecture, ) # Finally, delete the local copy our zip package @@ -1376,7 +1375,6 @@ def undeploy(self, no_confirm=False, remove_logs=False): if self.use_alb: self.zappa.undeploy_lambda_alb(self.lambda_name) - if self.use_apigateway: if remove_logs: self.zappa.remove_api_gateway_logs(self.lambda_name) diff --git a/zappa/core.py b/zappa/core.py index 76dc4b9c5..9d3306285 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1348,7 +1348,6 @@ def update_lambda_function( else: kwargs["S3Bucket"] = bucket kwargs["S3Key"] = s3_key - response = self.lambda_client.update_function_code(**kwargs) resource_arn = response["FunctionArn"] @@ -1445,7 +1444,7 @@ def update_lambda_configuration( layers=None, snap_start=None, capacity_provider_config=None, - wait=True + wait=True, ): """ Given an existing function ARN, update the configuration variables. @@ -1530,9 +1529,9 @@ def update_lambda_configuration( elif "capacity-provider:" in capacity_provider_name_part: capacity_provider_name_part = capacity_provider_name_part.split("capacity-provider:", 1)[1] capacity_provider_name = capacity_provider_name_part.rsplit("/", 1)[-1] - + versions_in_lambda = self.list_lambda_function_versions(function_name=function_name) - + if versions_in_lambda: # wait for latest version latest_version = max(int(v) for v in versions_in_lambda if v.isdigit()) diff --git a/zappa/handler.py b/zappa/handler.py index 03fca23a3..19d61c967 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -526,19 +526,6 @@ def handler(self, event, context): try: time_start = datetime.datetime.now() - script_name = "" - host = event.get("headers", {}).get("host") - if host: - if "amazonaws.com" in host: - logger.debug("amazonaws found in host") - # The path provided in th event doesn't include the - # stage, so we must tell Flask to include the API - # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 - script_name = f"/{settings.API_STAGE}" - # fix function url domain - if host.find("lambda-url") > -1 and event.get("headers", {}).get("cloudfront-host"): - # https://stackoverflow.com/questions/73024633/cloudfront-forward-host-header-to-lambda-function-url-origin - event["headers"]["host"] = event["headers"]["cloudfront-host"] # Determine if this is API Gateway v2 (has stage) or Function URL (no stage) # API Gateway v2 includes stage in requestContext request_context = event.get("requestContext", {}) @@ -550,7 +537,7 @@ def handler(self, event, context): if stage: # API Gateway v2 with named stage - rawPath includes the stage script_name = f"/{stage}" - if host.find("lambda-url") > -1: + else: # Function URL - no stage script_name = "" From 581ad40284032fcaf93ab90cacd122cd44449fb9 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 26 Jan 2026 15:06:54 +0800 Subject: [PATCH 103/111] cleanup commit --- tests/test_core.py | 19 ------------------- zappa/cli.py | 18 ------------------ zappa/core.py | 17 +++++++++++------ 3 files changed, 11 insertions(+), 43 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index a0de6d64d..1dab69de7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -225,14 +225,6 @@ def test_get_manylinux_python313(self): self.assertTrue(os.path.isfile(path)) os.remove(path) - def test_manylinux_pattern_python314(self): - z = Zappa(runtime="python3.14") - wheel_filename = "psycopg_binary-3.2.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - abi3_wheel_filename = "cryptography-44.0.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - - self.assertTrue(z.manylinux_wheel_file_match.match(wheel_filename)) - self.assertTrue(z.manylinux_wheel_file_match.match(abi3_wheel_filename)) - # same, but with an ABI3 package mock_installed_packages = {"cryptography": "44.0.2"} with mock.patch( @@ -4408,17 +4400,6 @@ def test_unsupported_version_error(self, *_): reload(zappa) - @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 14)) - def test_supported_python_version(self, *_): - from importlib import reload - - try: - import zappa - - reload(zappa) - except RuntimeError as exc: # pragma: no cover - self.fail(f"RuntimeError raised for supported Python version: {exc}") - @mock.patch("os.getenv", return_value="True") @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 6)) def test_minor_version_only_check_when_in_docker(self, *_): diff --git a/zappa/cli.py b/zappa/cli.py index 425e07c28..1b5c2644a 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -28,8 +28,6 @@ import botocore import click import hjson as json - -# import pkg_resources import requests import slugify import toml @@ -594,7 +592,6 @@ def dispatch_command(self, command, stage): + click.style(self.api_stage, bold=True) + ".." ) - click.echo(self.vargs) # Explicitly define the app function. # Related: https://github.com/Miserlou/Zappa/issues/832 if self.vargs.get("app_function", None): @@ -920,7 +917,6 @@ def deploy(self, source_zip=None, docker_image_uri=None): use_alb=self.use_alb, layers=self.layers, concurrency=self.lambda_concurrency, - architecture=self.architecture, ) kwargs["function_name"] = self.lambda_name if docker_image_uri: @@ -1034,8 +1030,6 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): """ Repackage and update the function code. """ - click.echo(self.stage_config) - click.echo(self.zappa.aws_region) if not source_zip and not docker_image_uri: # Make sure we're in a venv. self.check_venv() @@ -1432,12 +1426,6 @@ def schedule(self): Given a a list of functions and a schedule to execute them, setup up regular execution. """ - self.zappa.unschedule_events( - lambda_name=self.lambda_name, - lambda_arn=self.lambda_arn, - events=[], - excluded_source_services=["dynamodb", "kinesis", "sqs"], - ) events = self.stage_config.get("events", []) @@ -2730,9 +2718,6 @@ def load_settings(self, settings_file=None, session=None): # Additional tags self.tags = self.stage_config.get("tags", {}) - # Architectures - self.architecture = self.stage_config.get("architecture", "x86_64") - desired_role_name = self.lambda_name + "-ZappaLambdaExecutionRole" self.zappa = Zappa( boto_session=session, @@ -3450,9 +3435,6 @@ def touch_endpoint(self, endpoint_url): + " response code." ) - if req.status_code == 200: - click.echo(req.text) - #################################################################### # Main diff --git a/zappa/core.py b/zappa/core.py index 9d3306285..7f13d497a 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -3176,7 +3176,11 @@ def update_route53_records(self, domain_name, dns_name): # Related: https://github.com/boto/boto3/issues/157 # and: http://docs.aws.amazon.com/Route53/latest/APIReference/CreateAliasRRSAPI.html # and policy: https://spin.atomicobject.com/2016/04/28/route-53-hosted-zone-managment/ + # pure_zone_id = zone_id.split('/hostedzone/')[1] + # XXX: ClientError: An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: + # Tried to create an alias that targets d1awfeji80d0k2.cloudfront.net., type A in zone Z1XWOQP59BYF6Z, + # but the alias target name does not lie within the target zone response = self.route53.change_resource_record_sets( HostedZoneId=zone_id, ChangeBatch={"Changes": [{"Action": "UPSERT", "ResourceRecordSet": record_set}]}, @@ -3455,7 +3459,7 @@ def create_event_permission(self, lambda_name, principal, source_arn): permission_response = self.lambda_client.add_permission( FunctionName=lambda_name, - StatementId="zappa-" + "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)), + StatementId="".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)), Action="lambda:InvokeFunction", Principal=principal, SourceArn=source_arn, @@ -3486,17 +3490,18 @@ def schedule_events(self, lambda_arn, lambda_name, events, default=True): # and do not require event permissions. They do require additional permissions on the Lambda roles though. # http://docs.aws.amazon.com/lambda/latest/dg/lambda-api-permissions-ref.html pull_services = ["dynamodb", "kinesis", "sqs"] + + # XXX: Not available in Lambda yet. + # We probably want to execute the latest code. + # if default: + # lambda_arn = lambda_arn + ":$LATEST" + self.unschedule_events( lambda_name=lambda_name, lambda_arn=lambda_arn, events=events, excluded_source_services=pull_services, ) - # XXX: Not available in Lambda yet. - # We probably want to execute the latest code. - # if default: - # lambda_arn = lambda_arn + ":$LATEST" - for event in events: function = event["function"] expression = event.get("expression", None) # single expression From 8ec5c9670c5981e41c5162f57bd2ce0481690e00 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 26 Jan 2026 15:33:53 +0800 Subject: [PATCH 104/111] cleanup commit --- README.md | 1 + zappa/cli.py | 10 ++++++++-- zappa/core.py | 31 +++++++++++++------------------ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e1297e81e..78e68af49 100644 --- a/README.md +++ b/README.md @@ -1013,6 +1013,7 @@ to change Zappa's behavior. Use these at your own risk! "maxAge": 0 // The maximum amount of time, in seconds, that web browsers can cache results of a preflight request. default 0. } }, + "function_url_domains": ["yourapp.yourdomain.com"], // optional, list of custom domains to associate with the function URL. Requires certificate_arn to be set. // NOTE: Function URLs do NOT include stage names in their paths. Unlike API Gateway v1/v2 which include // the stage name in the URL (e.g., /dev/mypath), Function URLs route directly to your app (e.g., /mypath). // This means SCRIPT_NAME will be empty for Function URL requests, and PATH_INFO will contain the full path. diff --git a/zappa/cli.py b/zappa/cli.py index 1b5c2644a..c16b22745 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -592,6 +592,7 @@ def dispatch_command(self, command, stage): + click.style(self.api_stage, bold=True) + ".." ) + # Explicitly define the app function. # Related: https://github.com/Miserlou/Zappa/issues/832 if self.vargs.get("app_function", None): @@ -1383,6 +1384,11 @@ def undeploy(self, no_confirm=False, remove_logs=False): self.zappa.undeploy_api_gateway(self.lambda_name, domain_name=domain_name, base_path=base_path) + if self.use_function_url: + if self.function_url_domains: + self.zappa.undeploy_function_url_custom_domain(self.lambda_name) + self.zappa.delete_lambda_function_url(self.lambda_arn) + self.unschedule() # removes event triggers, including warm up event. self.zappa.delete_lambda_function(self.lambda_name) @@ -1426,7 +1432,6 @@ def schedule(self): Given a a list of functions and a schedule to execute them, setup up regular execution. """ - events = self.stage_config.get("events", []) if events: @@ -1450,6 +1455,7 @@ def schedule(self): "description": "Zappa Keep Warm - {}".format(self.lambda_name), } ) + if events: try: function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name) @@ -2395,7 +2401,7 @@ def certify(self, no_confirm=True, manual=False): # Custom SSL / ACM else: - if not self.zappa.get_domain_name(self.domain, route53=route53): + if not self.zappa.get_domain_name(self.domain, route53=route53) and (not self.function_url_enabled): dns_name = self.zappa.create_domain_name( domain_name=self.domain, certificate_name=self.domain + "-Zappa-Cert", diff --git a/zappa/core.py b/zappa/core.py index 7f13d497a..b0263e9d5 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -163,7 +163,7 @@ def build_manylinux_wheel_file_match_pattern(runtime: str, architecture: str) -> # Support PEP600 (https://peps.python.org/pep-0600/) # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl runtime_major_version, runtime_minor_version = runtime[6:].split(".") - python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.14 -> cp314 + python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.13 -> cp313 manylinux_legacy_tags = ("manylinux2014", "manylinux2010", "manylinux1") if architecture == X86_ARCHITECTURE: valid_platform_tags = [X86_ARCHITECTURE] @@ -246,7 +246,7 @@ def __init__( load_credentials=True, desired_role_name=None, desired_role_arn=None, - runtime="python3.14", # Detected at runtime in CLI + runtime="python3.13", # Detected at runtime in CLI tags=(), endpoint_urls={}, xray_tracing=False, @@ -2726,7 +2726,7 @@ def get_rest_apis(self, project_name): continue yield api - def undeploy_function_url_custom_domain(self, lambda_name, domains=None): + def undeploy_function_url_custom_domain(self, lambda_name): response = self.lambda_client.list_function_url_configs(FunctionName=lambda_name, MaxItems=50) if not response.get("FunctionUrlConfigs", []): @@ -3093,8 +3093,7 @@ def get_api_id(self, lambda_name: str, apigateway_version: str = DEFAULT_APIGATE if item["name"] == lambda_name: return item["id"] - logger.exception(f"Could not get API ID. {lambda_name} {self.boto_session.region_name}") - logger.exception(response) + logger.exception(f"Could not get API ID.") return None except Exception: # pragma: no cover # We don't even have an API deployed. That's okay! @@ -3115,7 +3114,6 @@ def create_domain_name( """ Creates the API GW domain and returns the resulting DNS name. """ - # This is a Let's Encrypt or custom certificate if not certificate_arn: agw_response = self.apigateway_client.create_domain_name( @@ -3132,19 +3130,16 @@ def create_domain_name( certificateName=certificate_name, certificateArn=certificate_arn, ) - api_id = self.get_api_id(lambda_name) - if not api_id: - raise LookupError("No API URL to certify found - did you deploy?") - - self.apigateway_client.create_base_path_mapping( - domainName=domain_name, - basePath="" if base_path is None else base_path, - restApiId=api_id, - stage=stage, - ) + api_id = self.get_api_id(lambda_name) + if not api_id: + raise LookupError("No API URL to certify found - did you deploy?") - if self.function_url_enabled: - pass + self.apigateway_client.create_base_path_mapping( + domainName=domain_name, + basePath="" if base_path is None else base_path, + restApiId=api_id, + stage=stage, + ) return agw_response["distributionDomainName"] From 4e9885c32523efbe35b6ceefbdbd101918f39f55 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 26 Jan 2026 15:51:18 +0800 Subject: [PATCH 105/111] cleanup commit --- zappa/cli.py | 13 +++++++------ zappa/core.py | 10 ++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index c16b22745..aed1e1148 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1031,6 +1031,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): """ Repackage and update the function code. """ + if not source_zip and not docker_image_uri: # Make sure we're in a venv. self.check_venv() @@ -2401,7 +2402,7 @@ def certify(self, no_confirm=True, manual=False): # Custom SSL / ACM else: - if not self.zappa.get_domain_name(self.domain, route53=route53) and (not self.function_url_enabled): + if not self.zappa.get_domain_name(self.domain, route53=route53): dns_name = self.zappa.create_domain_name( domain_name=self.domain, certificate_name=self.domain + "-Zappa-Cert", @@ -2453,11 +2454,11 @@ def certify(self, no_confirm=True, manual=False): if route53: for domain in self.function_url_domains: self.zappa.update_route53_records(domain, dns_name) - print( - "Created a new domain name with supplied certificate. " - "Please note that it can take up to 40 minutes for this domain to be " - "created and propagated through AWS, but it requires no further work on your part." - ) + print( + "Created a new domain name with supplied certificate. " + "Please note that it can take up to 40 minutes for this domain to be " + "created and propagated through AWS, but it requires no further work on your part." + ) ## # Shell diff --git a/zappa/core.py b/zappa/core.py index b0263e9d5..75a3d33ff 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -16,7 +16,6 @@ import tarfile import tempfile import time -import urllib import uuid import zipfile from builtins import bytes, int @@ -1449,7 +1448,6 @@ def update_lambda_configuration( """ Given an existing function ARN, update the configuration variables. """ - logger.info("Updating Lambda function configuration..") if not vpc_config: @@ -3093,7 +3091,7 @@ def get_api_id(self, lambda_name: str, apigateway_version: str = DEFAULT_APIGATE if item["name"] == lambda_name: return item["id"] - logger.exception(f"Could not get API ID.") + logger.exception("Could not get API ID.") return None except Exception: # pragma: no cover # We don't even have an API deployed. That's okay! @@ -3114,6 +3112,7 @@ def create_domain_name( """ Creates the API GW domain and returns the resulting DNS name. """ + # This is a Let's Encrypt or custom certificate if not certificate_arn: agw_response = self.apigateway_client.create_domain_name( @@ -3130,6 +3129,7 @@ def create_domain_name( certificateName=certificate_name, certificateArn=certificate_arn, ) + api_id = self.get_api_id(lambda_name) if not api_id: raise LookupError("No API URL to certify found - did you deploy?") @@ -3230,8 +3230,6 @@ def update_domain_name( stage=None, route53=True, base_path=None, - use_apigateway=True, - use_function_url=False, ): """ This updates your certificate information for an existing domain, @@ -3258,7 +3256,7 @@ def update_domain_name( ) certificate_arn = acm_certificate["CertificateArn"] - if use_apigateway: + if self.apigateway: self.update_domain_base_path_mapping(domain_name, lambda_name, stage, base_path) res = self.apigateway_client.update_domain_name( From 342e0bdcd45412f8590747b7dde24cb7a66c5d56 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 26 Jan 2026 15:56:30 +0800 Subject: [PATCH 106/111] cleanup commit --- zappa/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zappa/core.py b/zappa/core.py index 75a3d33ff..171fac19c 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -3274,8 +3274,7 @@ def update_domain_name( }, ], ) - if use_function_url: - pass + return res def update_domain_base_path_mapping(self, domain_name, lambda_name, stage, base_path): From b6ba7fcbab21bb9ef092f239932f83bc5953b000 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 26 Jan 2026 16:00:18 +0800 Subject: [PATCH 107/111] fix test cases --- zappa/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zappa/core.py b/zappa/core.py index 171fac19c..a0feaebe5 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -16,6 +16,7 @@ import tarfile import tempfile import time +import urllib import uuid import zipfile from builtins import bytes, int @@ -1870,7 +1871,6 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains if not response.get("FunctionUrlConfigs", []): logger.info("no function url configured on lambda, skip setting custom domains") url = response["FunctionUrlConfigs"][0]["FunctionUrl"] - import urllib url = urllib.parse.urlparse(url) From 2a1b8b0419be4a5587688b6ba58c78f1833a61b6 Mon Sep 17 00:00:00 2001 From: versionbump Date: Mon, 26 Jan 2026 16:12:52 +0800 Subject: [PATCH 108/111] fix test cases --- zappa/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zappa/cli.py b/zappa/cli.py index aed1e1148..53683b1e8 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -2454,11 +2454,11 @@ def certify(self, no_confirm=True, manual=False): if route53: for domain in self.function_url_domains: self.zappa.update_route53_records(domain, dns_name) - print( - "Created a new domain name with supplied certificate. " - "Please note that it can take up to 40 minutes for this domain to be " - "created and propagated through AWS, but it requires no further work on your part." - ) + print( + "Created a new domain name with supplied certificate. " + "Please note that it can take up to 40 minutes for this domain to be " + "created and propagated through AWS, but it requires no further work on your part." + ) ## # Shell From 3de14532d37c558b9471c73b454c7f02bd0ee42f Mon Sep 17 00:00:00 2001 From: versionbump Date: Tue, 24 Feb 2026 18:46:39 +0800 Subject: [PATCH 109/111] allow config publish to latest alias --- README.md | 1 + tests/test_core.py | 47 ++++++++++++++++++++++++++++++++++++++++ tests/test_settings.yaml | 3 +++ zappa/cli.py | 5 +++++ zappa/core.py | 23 ++++++++++++-------- 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 78e68af49..a41a9cad6 100644 --- a/README.md +++ b/README.md @@ -1103,6 +1103,7 @@ to change Zappa's behavior. Use these at your own risk! "layers": ["arn:aws:lambda:::layer::"], // optional lambda layers "lambda_concurrency": 10, // Sets the maximum number of simultaneous executions for a function, and reserves capacity for that concurrency level. Default is None. Not supported when using capacity providers. "capacity_provider_config": { "LambdaManagedInstancesCapacityProviderConfig": { "CapacityProviderArn": "arn:aws:lambda:::capacity-provider/", "PerExecutionEnvironmentMaxConcurrency": 10, "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0 } }, // Configure the Lambda capacity provider used for your function. Optional. Not compatible with VPC configurations. + "capacity_provider_publish_to_latest_published": false, // When using capacity providers, publish a version to $LATEST.PUBLISHED after configuration updates. Default false. "lets_encrypt_key": "s3://your-bucket/account.key", // Let's Encrypt account key path. Can either be an S3 path or a local file path. "log_level": "DEBUG", // Set the Zappa log level. Can be one of CRITICAL, ERROR, WARNING, INFO and DEBUG. Default: DEBUG "manage_roles": true, // Have Zappa automatically create and define IAM execution roles and policies. Default true. If false, you must define your own IAM Role and role_name setting. diff --git a/tests/test_core.py b/tests/test_core.py index 1dab69de7..b46137318 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -881,6 +881,9 @@ def test_update_capacity_provider_configuration(self): ], "NextMarker": "", } + z.lambda_client.list_versions_by_function.return_value = { + "Versions": [{"Version": "$LATEST.PUBLISHED"}, {"Version": "1"}] + } z.lambda_client.publish_version.return_value = { "FunctionArn": "test", } @@ -904,6 +907,43 @@ def test_update_capacity_provider_configuration(self): z.lambda_client.update_function_configuration.call_args[1]["CapacityProviderConfig"], capacity_provider_config, ) + z.lambda_client.delete_function.assert_not_called() + z.lambda_client.publish_version.assert_not_called() + + def test_update_capacity_provider_configuration_publish_to_latest_published(self): + z = Zappa(load_credentials=False) + z.credentials_arn = object() + z.lambda_client = mock.MagicMock() + z.lambda_client.get_function_configuration.return_value = {"PackageType": "Zip"} + capacity_provider_arn = "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test" + z.lambda_client.list_function_versions_by_capacity_provider.return_value = { + "CapacityProviderArn": capacity_provider_arn, + "FunctionVersions": [{"FunctionArn": "test", "State": "Active"}], + "NextMarker": "", + } + z.lambda_client.list_versions_by_function.return_value = { + "Versions": [{"Version": "$LATEST"}, {"Version": "1"}, {"Version": "$LATEST.PUBLISHED"}] + } + z.lambda_client.publish_version.return_value = {"FunctionArn": "test"} + z.wait_until_lambda_function_is_updated = mock.MagicMock() + capacity_provider_config = { + "LambdaManagedInstancesCapacityProviderConfig": { + "CapacityProviderArn": capacity_provider_arn, + "PerExecutionEnvironmentMaxConcurrency": 10, + "ExecutionEnvironmentMemoryGiBPerVCpu": 4.0, + } + } + + z.update_lambda_configuration( + "test", + "test", + "test", + capacity_provider_config=capacity_provider_config, + capacity_provider_publish_to_latest_published=True, + ) + + z.lambda_client.delete_function.assert_called_once_with(FunctionName="test", Qualifier="$LATEST.PUBLISHED") + z.lambda_client.publish_version.assert_called_once_with(FunctionName="test", PublishTo="LATEST_PUBLISHED") def test_update_capacity_provider_rejects_vpc(self): z = Zappa(load_credentials=False) @@ -953,6 +993,13 @@ def test_capacity_provider_configuration(self): } } self.assertEqual(expected_config, zappa_cli.capacity_provider_config) + self.assertFalse(zappa_cli.capacity_provider_publish_to_latest_published) + + def test_capacity_provider_publish_to_latest_published_configuration(self): + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "capacity_provider_publish_latest_published_enabled" + zappa_cli.load_settings("tests/test_settings.yaml") + self.assertTrue(zappa_cli.capacity_provider_publish_to_latest_published) def test_update_empty_aws_env_hash(self): z = Zappa() diff --git a/tests/test_settings.yaml b/tests/test_settings.yaml index c5e91dede..878b27f6f 100644 --- a/tests/test_settings.yaml +++ b/tests/test_settings.yaml @@ -67,3 +67,6 @@ capacity_provider_enabled: CapacityProviderArn: arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test PerExecutionEnvironmentMaxConcurrency: 5 ExecutionEnvironmentMemoryGiBPerVCpu: 2.0 +capacity_provider_publish_latest_published_enabled: + extends: capacity_provider_enabled + capacity_provider_publish_to_latest_published: true diff --git a/zappa/cli.py b/zappa/cli.py index 53683b1e8..677fd3e7b 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -126,6 +126,7 @@ class ZappaCLI: aws_kms_key_arn = "" snap_start = None capacity_provider_config = None + capacity_provider_publish_to_latest_published = False context_header_mappings = None additional_text_mimetypes = None tags = [] # type: ignore[var-annotated] @@ -1200,6 +1201,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): layers=self.layers, snap_start=self.snap_start, capacity_provider_config=self.capacity_provider_config, + capacity_provider_publish_to_latest_published=self.capacity_provider_publish_to_latest_published, wait=False, ) @@ -2689,6 +2691,9 @@ def load_settings(self, settings_file=None, session=None): self.aws_kms_key_arn = self.stage_config.get("aws_kms_key_arn", "") self.snap_start = self.stage_config.get("snap_start", "None") self.capacity_provider_config = self.stage_config.get("capacity_provider_config", None) + self.capacity_provider_publish_to_latest_published = self.stage_config.get( + "capacity_provider_publish_to_latest_published", False + ) self.context_header_mappings = self.stage_config.get("context_header_mappings", {}) self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") diff --git a/zappa/core.py b/zappa/core.py index a0feaebe5..d38d1c498 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1444,6 +1444,7 @@ def update_lambda_configuration( layers=None, snap_start=None, capacity_provider_config=None, + capacity_provider_publish_to_latest_published=False, wait=True, ): """ @@ -1542,16 +1543,20 @@ def update_lambda_configuration( function_state="Active", ) - # publish to latest - response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo="LATEST_PUBLISHED") - logger.info(f"Publish to {response['FunctionArn']}") + if capacity_provider_publish_to_latest_published: + if "$LATEST.PUBLISHED" in versions_in_lambda: + self.lambda_client.delete_function(FunctionName=function_name, Qualifier="$LATEST.PUBLISHED") - time.sleep(10) - self.wait_for_capacity_provider_response( - capacity_provider_name=capacity_provider_name, - function_arn=response["FunctionArn"], - function_state="Active", - ) + # publish to latest + response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo="LATEST_PUBLISHED") + logger.info(f"Publish to {response['FunctionArn']}") + + time.sleep(10) + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=response["FunctionArn"], + function_state="Active", + ) if self.tags: self.lambda_client.tag_resource(Resource=resource_arn, Tags=self.tags) From 0c19fc56a0fe5b7288a913099907e0984c96fa93 Mon Sep 17 00:00:00 2001 From: sha Date: Tue, 24 Feb 2026 22:54:38 +0800 Subject: [PATCH 110/111] Revert snapshot to 2a1b8b04 --- README.md | 7 +- example/authmodule.py | 6 +- test | 5 -- test_settings.json | 9 --- tests/test_core.py | 66 ------------------- tests/test_handler.py | 5 +- tests/test_settings.yaml | 3 - tests/test_utilities.py | 5 +- zappa/__init__.py | 8 +-- zappa/asynchronous.py | 1 - zappa/cli.py | 34 +--------- zappa/core.py | 136 ++++++++++++--------------------------- zappa/handler.py | 15 +---- zappa/utilities.py | 29 ++------- 14 files changed, 59 insertions(+), 270 deletions(-) delete mode 100755 test diff --git a/README.md b/README.md index 3e87b3508..78e68af49 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ And finally, Zappa is **super easy to use**. You can deploy your application wit ## Installation and Configuration -_Before you begin, make sure you are running Python 3.8/3.9/3.10/3.11/3.12/3.13 and you have a valid AWS account and your [AWS credentials file](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) is properly installed._ +_Before you begin, make sure you are running Python 3.9/3.10/3.11/3.12/3.13/3.14 and you have a valid AWS account and your [AWS credentials file](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) is properly installed._ **Zappa** can easily be installed through pip, like so: @@ -538,7 +538,7 @@ For instance, suppose you have a basic application in a file called "my_app.py", Any remote print statements made and the value the function returned will then be printed to your local console. **Nifty!** -You can also invoke interpretable Python 3.8/3.9/3.10/3.11/3.12/3.13 strings directly by using `--raw`, like so: +You can also invoke interpretable Python 3.9/3.10/3.11/3.12/3.13/3.14 strings directly by using `--raw`, like so: $ zappa invoke production "print(1 + 2 + 3)" --raw @@ -1103,7 +1103,6 @@ to change Zappa's behavior. Use these at your own risk! "layers": ["arn:aws:lambda:::layer::"], // optional lambda layers "lambda_concurrency": 10, // Sets the maximum number of simultaneous executions for a function, and reserves capacity for that concurrency level. Default is None. Not supported when using capacity providers. "capacity_provider_config": { "LambdaManagedInstancesCapacityProviderConfig": { "CapacityProviderArn": "arn:aws:lambda:::capacity-provider/", "PerExecutionEnvironmentMaxConcurrency": 10, "ExecutionEnvironmentMemoryGiBPerVCpu": 2.0 } }, // Configure the Lambda capacity provider used for your function. Optional. Not compatible with VPC configurations. - "capacity_provider_publish_to_latest_published": false, // When using capacity providers, publish a version to $LATEST.PUBLISHED after configuration updates. Default false. "lets_encrypt_key": "s3://your-bucket/account.key", // Let's Encrypt account key path. Can either be an S3 path or a local file path. "log_level": "DEBUG", // Set the Zappa log level. Can be one of CRITICAL, ERROR, WARNING, INFO and DEBUG. Default: DEBUG "manage_roles": true, // Have Zappa automatically create and define IAM execution roles and policies. Default true. If false, you must define your own IAM Role and role_name setting. @@ -1120,7 +1119,7 @@ to change Zappa's behavior. Use these at your own risk! "role_name": "MyLambdaRole", // Name of Zappa execution role. Default --ZappaExecutionRole. To use a different, pre-existing policy, you must also set manage_roles to false. "role_arn": "arn:aws:iam::12345:role/app-ZappaLambdaExecutionRole", // ARN of Zappa execution role. Default to None. To use a different, pre-existing policy, you must also set manage_roles to false. This overrides role_name. Use with temporary credentials via GetFederationToken. "route53_enabled": true, // Have Zappa update your Route53 Hosted Zones when certifying with a custom domain. Default true. - "runtime": "python3.13", // Python runtime to use on Lambda. Can be one of: "python3.8", "python3.9", "python3.10", "python3.11", "python3.12", or "python3.13". Defaults to whatever the current Python being used is. + "runtime": "python3.14", // Python runtime to use on Lambda. Can be one of: "python3.9", "python3.10", "python3.11", "python3.12", "python3.13", or "python3.14". Defaults to whatever the current Python being used is. "s3_bucket": "dev-bucket", // Zappa zip bucket, "slim_handler": false, // Useful if project >50M. Set true to just upload a small handler to Lambda and load actual project from S3 at runtime. Default false. "snap_start": "PublishedVersions", // Enable Lambda SnapStart for faster cold starts. Can be "PublishedVersions" or "None". Default "None". diff --git a/example/authmodule.py b/example/authmodule.py index 8f936182e..a11b07e90 100644 --- a/example/authmodule.py +++ b/example/authmodule.py @@ -188,15 +188,13 @@ def denyMethod(self, verb, resource): def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy - conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition - """ + conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy - conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition - """ + conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): diff --git a/test b/test deleted file mode 100755 index b716c3d27..000000000 --- a/test +++ /dev/null @@ -1,5 +0,0 @@ -#! /bin/bash -pytest --cov=zappa - -# For a specific test: -# pytest tests/tests.py::TestZappa::test_lets_encrypt_sanity diff --git a/test_settings.json b/test_settings.json index 7bc3ae6cb..8d15d0378 100644 --- a/test_settings.json +++ b/test_settings.json @@ -154,15 +154,6 @@ "binary_support": true, "additional_text_mimetypes": ["application/custommimetype"] }, - "function_url_enabled": { - "extends": "ttt888", - "function_url_enabled": true - }, - "function_url_custom_domain": { - "extends": "function_url_enabled", - "function_url_domains": ["test-lambda-function-url.example.com", "test-lambda-function-url-1.example.com"], - "route53_enabled": true - }, "arch_arm64": { "s3_bucket": "lmbda", "app_function": "tests.test_app.hello_world", diff --git a/tests/test_core.py b/tests/test_core.py index 736cc514a..1dab69de7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -225,14 +225,6 @@ def test_get_manylinux_python313(self): self.assertTrue(os.path.isfile(path)) os.remove(path) - def test_manylinux_pattern_python314(self): - z = Zappa(runtime="python3.14") - wheel_filename = "psycopg_binary-3.2.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - abi3_wheel_filename = "cryptography-44.0.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - - self.assertTrue(z.manylinux_wheel_file_match.match(wheel_filename)) - self.assertTrue(z.manylinux_wheel_file_match.match(abi3_wheel_filename)) - # same, but with an ABI3 package mock_installed_packages = {"cryptography": "44.0.2"} with mock.patch( @@ -889,9 +881,6 @@ def test_update_capacity_provider_configuration(self): ], "NextMarker": "", } - z.lambda_client.list_versions_by_function.return_value = { - "Versions": [{"Version": "$LATEST.PUBLISHED"}, {"Version": "1"}] - } z.lambda_client.publish_version.return_value = { "FunctionArn": "test", } @@ -915,43 +904,6 @@ def test_update_capacity_provider_configuration(self): z.lambda_client.update_function_configuration.call_args[1]["CapacityProviderConfig"], capacity_provider_config, ) - z.lambda_client.delete_function.assert_not_called() - z.lambda_client.publish_version.assert_not_called() - - def test_update_capacity_provider_configuration_publish_to_latest_published(self): - z = Zappa(load_credentials=False) - z.credentials_arn = object() - z.lambda_client = mock.MagicMock() - z.lambda_client.get_function_configuration.return_value = {"PackageType": "Zip"} - capacity_provider_arn = "arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test" - z.lambda_client.list_function_versions_by_capacity_provider.return_value = { - "CapacityProviderArn": capacity_provider_arn, - "FunctionVersions": [{"FunctionArn": "test", "State": "Active"}], - "NextMarker": "", - } - z.lambda_client.list_versions_by_function.return_value = { - "Versions": [{"Version": "$LATEST"}, {"Version": "1"}, {"Version": "$LATEST.PUBLISHED"}] - } - z.lambda_client.publish_version.return_value = {"FunctionArn": "test"} - z.wait_until_lambda_function_is_updated = mock.MagicMock() - capacity_provider_config = { - "LambdaManagedInstancesCapacityProviderConfig": { - "CapacityProviderArn": capacity_provider_arn, - "PerExecutionEnvironmentMaxConcurrency": 10, - "ExecutionEnvironmentMemoryGiBPerVCpu": 4.0, - } - } - - z.update_lambda_configuration( - "test", - "test", - "test", - capacity_provider_config=capacity_provider_config, - capacity_provider_publish_to_latest_published=True, - ) - - z.lambda_client.delete_function.assert_called_once_with(FunctionName="test", Qualifier="$LATEST.PUBLISHED") - z.lambda_client.publish_version.assert_called_once_with(FunctionName="test", PublishTo="LATEST_PUBLISHED") def test_update_capacity_provider_rejects_vpc(self): z = Zappa(load_credentials=False) @@ -1001,13 +953,6 @@ def test_capacity_provider_configuration(self): } } self.assertEqual(expected_config, zappa_cli.capacity_provider_config) - self.assertFalse(zappa_cli.capacity_provider_publish_to_latest_published) - - def test_capacity_provider_publish_to_latest_published_configuration(self): - zappa_cli = ZappaCLI() - zappa_cli.api_stage = "capacity_provider_publish_latest_published_enabled" - zappa_cli.load_settings("tests/test_settings.yaml") - self.assertTrue(zappa_cli.capacity_provider_publish_to_latest_published) def test_update_empty_aws_env_hash(self): z = Zappa() @@ -4455,17 +4400,6 @@ def test_unsupported_version_error(self, *_): reload(zappa) - @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 14)) - def test_supported_python_version(self, *_): - from importlib import reload - - try: - import zappa - - reload(zappa) - except RuntimeError as exc: # pragma: no cover - self.fail(f"RuntimeError raised for supported Python version: {exc}") - @mock.patch("os.getenv", return_value="True") @mock.patch("sys.version_info", new_callable=partial(get_sys_versioninfo, 6)) def test_minor_version_only_check_when_in_docker(self, *_): diff --git a/tests/test_handler.py b/tests/test_handler.py index 59a36c6a4..353baf5d7 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -600,10 +600,7 @@ def test_wsgi_script_name_on_v2_formatted_event_function_url(self): }, "isBase64Encoded": False, "body": "", - "cookies": [ - "Cookie_1=Value1; Expires=21 Oct 2021 07:48 GMT", - "Cookie_2=Value2; Max-Age=78000", - ], + "cookies": ["Cookie_1=Value1; Expires=21 Oct 2021 07:48 GMT", "Cookie_2=Value2; Max-Age=78000"], } response = lh.handler(event, None) diff --git a/tests/test_settings.yaml b/tests/test_settings.yaml index 878b27f6f..c5e91dede 100644 --- a/tests/test_settings.yaml +++ b/tests/test_settings.yaml @@ -67,6 +67,3 @@ capacity_provider_enabled: CapacityProviderArn: arn:aws:lambda:us-east-1:123456789012:capacity-provider/zappa-test PerExecutionEnvironmentMaxConcurrency: 5 ExecutionEnvironmentMemoryGiBPerVCpu: 2.0 -capacity_provider_publish_latest_published_enabled: - extends: capacity_provider_enabled - capacity_provider_publish_to_latest_published: true diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 0f0bd2556..e6652f8c2 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -44,10 +44,7 @@ def tearDown(self): # Give the user their AWS region back, we're done testing with us-east-1. os.environ["AWS_DEFAULT_REGION"] = self.users_current_region_name - @mock.patch( - "zappa.core.find_packages", - return_value=["package", "package.subpackage", "package.another"], - ) + @mock.patch("zappa.core.find_packages", return_value=["package", "package.subpackage", "package.another"]) def test_copy_editable_packages(self, mock_find_packages): virtual_env = Path(os.environ.get("VIRTUAL_ENV")) if not virtual_env: diff --git a/zappa/__init__.py b/zappa/__init__.py index ab858501e..de821c814 100644 --- a/zappa/__init__.py +++ b/zappa/__init__.py @@ -8,13 +8,7 @@ def running_in_docker() -> bool: - When docker is used allow usage of any python version """ # https://stackoverflow.com/questions/63116419 - running_in_docker_flag = os.getenv("ZAPPA_RUNNING_IN_DOCKER", "False").lower() in { - "y", - "yes", - "t", - "true", - "1", - } + running_in_docker_flag = os.getenv("ZAPPA_RUNNING_IN_DOCKER", "False").lower() in {"y", "yes", "t", "true", "1"} return running_in_docker_flag diff --git a/zappa/asynchronous.py b/zappa/asynchronous.py index a69411679..0ba53ebff 100644 --- a/zappa/asynchronous.py +++ b/zappa/asynchronous.py @@ -296,7 +296,6 @@ def run_message(message): "ttl": {"N": str(int(time.time() + 600))}, "async_status": {"S": "in progress"}, "async_response": {"S": str(json.dumps("N/A"))}, - "message": {"S": json.dumps(message)}, }, ) diff --git a/zappa/cli.py b/zappa/cli.py index b2c936c73..53683b1e8 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -28,8 +28,6 @@ import botocore import click import hjson as json - -# import pkg_resources import requests import slugify import toml @@ -128,7 +126,6 @@ class ZappaCLI: aws_kms_key_arn = "" snap_start = None capacity_provider_config = None - capacity_provider_publish_to_latest_published = False context_header_mappings = None additional_text_mimetypes = None tags = [] # type: ignore[var-annotated] @@ -205,9 +202,6 @@ def handle(self, argv=None): desc = "Zappa - Deploy Python applications to AWS Lambda" " and API Gateway.\n" parser = argparse.ArgumentParser(description=desc) - from importlib.metadata import version - - zappa_version = version("zappa") parser.add_argument( "-v", "--version", @@ -598,7 +592,7 @@ def dispatch_command(self, command, stage): + click.style(self.api_stage, bold=True) + ".." ) - click.echo(self.vargs) + # Explicitly define the app function. # Related: https://github.com/Miserlou/Zappa/issues/832 if self.vargs.get("app_function", None): @@ -924,7 +918,6 @@ def deploy(self, source_zip=None, docker_image_uri=None): use_alb=self.use_alb, layers=self.layers, concurrency=self.lambda_concurrency, - architecture=self.architecture, ) kwargs["function_name"] = self.lambda_name if docker_image_uri: @@ -1038,8 +1031,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): """ Repackage and update the function code. """ - click.echo(self.stage_config) - click.echo(self.zappa.aws_region) + if not source_zip and not docker_image_uri: # Make sure we're in a venv. self.check_venv() @@ -1208,9 +1200,7 @@ def update(self, source_zip=None, no_upload=False, docker_image_uri=None): layers=self.layers, snap_start=self.snap_start, capacity_provider_config=self.capacity_provider_config, - capacity_provider_publish_to_latest_published=self.capacity_provider_publish_to_latest_published, wait=False, - architecture=self.architecture, ) # Finally, delete the local copy our zip package @@ -1381,9 +1371,6 @@ def undeploy(self, no_confirm=False, remove_logs=False): if self.use_alb: self.zappa.undeploy_lambda_alb(self.lambda_name) - # if self.function_url_domains: - # self.zappa.undeploy_function_url_custom_domain(self.lambda_name) - if self.use_apigateway: if remove_logs: self.zappa.remove_api_gateway_logs(self.lambda_name) @@ -1446,13 +1433,6 @@ def schedule(self): Given a a list of functions and a schedule to execute them, setup up regular execution. """ - self.zappa.unschedule_events( - lambda_name=self.lambda_name, - lambda_arn=self.lambda_arn, - events=[], - excluded_source_services=["dynamodb", "kinesis", "sqs"], - ) - events = self.stage_config.get("events", []) if events: @@ -1476,6 +1456,7 @@ def schedule(self): "description": "Zappa Keep Warm - {}".format(self.lambda_name), } ) + if events: try: function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name) @@ -2708,9 +2689,6 @@ def load_settings(self, settings_file=None, session=None): self.aws_kms_key_arn = self.stage_config.get("aws_kms_key_arn", "") self.snap_start = self.stage_config.get("snap_start", "None") self.capacity_provider_config = self.stage_config.get("capacity_provider_config", None) - self.capacity_provider_publish_to_latest_published = self.stage_config.get( - "capacity_provider_publish_to_latest_published", False - ) self.context_header_mappings = self.stage_config.get("context_header_mappings", {}) self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") @@ -2747,9 +2725,6 @@ def load_settings(self, settings_file=None, session=None): # Additional tags self.tags = self.stage_config.get("tags", {}) - # Architectures - self.architecture = self.stage_config.get("architecture", "x86_64") - desired_role_name = self.lambda_name + "-ZappaLambdaExecutionRole" self.zappa = Zappa( boto_session=session, @@ -3467,9 +3442,6 @@ def touch_endpoint(self, endpoint_url): + " response code." ) - if req.status_code == 200: - click.echo(req.text) - #################################################################### # Main diff --git a/zappa/core.py b/zappa/core.py index 4e77620d8..a0feaebe5 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -2,7 +2,6 @@ Zappa core library. You may also want to look at `cli.py` and `util.py`. """ -import datetime import getpass import hashlib import json @@ -164,7 +163,7 @@ def build_manylinux_wheel_file_match_pattern(runtime: str, architecture: str) -> # Support PEP600 (https://peps.python.org/pep-0600/) # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl runtime_major_version, runtime_minor_version = runtime[6:].split(".") - python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.14 -> cp314 + python_tag = f"cp{runtime_major_version}{runtime_minor_version}" # python3.13 -> cp313 manylinux_legacy_tags = ("manylinux2014", "manylinux2010", "manylinux1") if architecture == X86_ARCHITECTURE: valid_platform_tags = [X86_ARCHITECTURE] @@ -247,7 +246,7 @@ def __init__( load_credentials=True, desired_role_name=None, desired_role_arn=None, - runtime="python3.14", # Detected at runtime in CLI + runtime="python3.13", # Detected at runtime in CLI tags=(), endpoint_urls={}, xray_tracing=False, @@ -271,9 +270,6 @@ def __init__( if desired_role_arn: self.credentials_arn = desired_role_arn - if architecture: - self.architecture = architecture - self.runtime = runtime if not architecture: @@ -402,10 +398,7 @@ def get_deps_list(self, pkg_name: str, installed_distros: Optional[Iterable] = N for ( requirement_package_name ) in distribution_package.requires: # Generated requirements specified for this Distribution - deps += self.get_deps_list( - pkg_name=requirement_package_name, - installed_distros=installed_distros, - ) + deps += self.get_deps_list(pkg_name=requirement_package_name, installed_distros=installed_distros) return list(set(deps)) # de-dupe before returning def create_handler_venv(self, use_zappa_release: Optional[str] = None): @@ -667,12 +660,7 @@ def splitpath(path): ignore=shutil.ignore_patterns(*excludes), ) else: - copytree( - site_packages_64.resolve(), - temp_package_path.resolve(), - metadata=False, - symlinks=False, - ) + copytree(site_packages_64.resolve(), temp_package_path.resolve(), metadata=False, symlinks=False) if egg_links: self.copy_editable_packages(egg_links, temp_package_path) @@ -1218,8 +1206,8 @@ def create_lambda_function( ephemeral_storage={"Size": 512}, publish=True, vpc_config=None, - efs_config=None, dead_letter_config=None, + efs_config=None, runtime="python3.13", aws_environment_variables=None, aws_kms_key_arn=None, @@ -1231,7 +1219,6 @@ def create_lambda_function( layers=None, concurrency=None, docker_image_uri=None, - architecture=None, ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, @@ -1251,16 +1238,6 @@ def create_lambda_function( aws_kms_key_arn = "" if not layers: layers = [] - if not architecture: - self.architecture = "x86_64" - - uses_capacity_provider = bool(capacity_provider_config) - uses_vpc = bool(vpc_config and (vpc_config.get("SubnetIds") or vpc_config.get("SecurityGroupIds"))) - if uses_capacity_provider and uses_vpc: - raise ValueError( - "Lambda capacity providers cannot be used with VPC configurations. " - "Remove VPC settings or disable the capacity provider." - ) uses_capacity_provider = bool(capacity_provider_config) uses_vpc = bool(vpc_config and (vpc_config.get("SubnetIds") or vpc_config.get("SecurityGroupIds"))) @@ -1355,7 +1332,6 @@ def update_lambda_function( concurrency=None, capacity_provider_config=None, docker_image_uri=None, - architecture=None, ): """ Given a bucket and key (or a local path) of a valid Lambda-zip, @@ -1468,14 +1444,11 @@ def update_lambda_configuration( layers=None, snap_start=None, capacity_provider_config=None, - capacity_provider_publish_to_latest_published=False, wait=True, - architecture=None, ): """ Given an existing function ARN, update the configuration variables. """ - logger.info("Updating Lambda function configuration..") if not vpc_config: @@ -1569,20 +1542,16 @@ def update_lambda_configuration( function_state="Active", ) - if capacity_provider_publish_to_latest_published: - if "$LATEST.PUBLISHED" in versions_in_lambda: - self.lambda_client.delete_function(FunctionName=function_name, Qualifier="$LATEST.PUBLISHED") - - # publish to latest - response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo="LATEST_PUBLISHED") - logger.info(f"Publish to {response['FunctionArn']}") + # publish to latest + response = self.lambda_client.publish_version(FunctionName=function_name, PublishTo="LATEST_PUBLISHED") + logger.info(f"Publish to {response['FunctionArn']}") - time.sleep(10) - self.wait_for_capacity_provider_response( - capacity_provider_name=capacity_provider_name, - function_arn=response["FunctionArn"], - function_state="Active", - ) + time.sleep(10) + self.wait_for_capacity_provider_response( + capacity_provider_name=capacity_provider_name, + function_arn=response["FunctionArn"], + function_state="Active", + ) if self.tags: self.lambda_client.tag_resource(Resource=resource_arn, Tags=self.tags) @@ -1909,10 +1878,7 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains config = { "CallerReference": "zappa-create-function-url-custom-domain-" + function_name.split(":")[-1], - "Aliases": { - "Quantity": len(function_url_domains), - "Items": function_url_domains, - }, + "Aliases": {"Quantity": len(function_url_domains), "Items": function_url_domains}, "DefaultRootObject": "", "Enabled": True, "PriceClass": "PriceClass_100", @@ -1928,10 +1894,7 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains "CustomHeaders": { "Quantity": 1, "Items": [ - { - "HeaderName": "CloudFront", - "HeaderValue": "CloudFront", - }, + {"HeaderName": "CloudFront", "HeaderValue": "CloudFront"}, ], }, "CustomOriginConfig": { @@ -1956,29 +1919,13 @@ def update_lambda_function_url_domains(self, function_name, function_url_domains "FieldLevelEncryptionId": "", "AllowedMethods": { "Quantity": 7, - "Items": [ - "HEAD", - "DELETE", - "POST", - "GET", - "OPTIONS", - "PUT", - "PATCH", - ], - "CachedMethods": { - "Quantity": 3, - "Items": ["HEAD", "GET", "OPTIONS"], - }, + "Items": ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"], + "CachedMethods": {"Quantity": 3, "Items": ["HEAD", "GET", "OPTIONS"]}, }, "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", # noqa: E501 https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac", # noqa: E501 https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-except-host-header }, - "Logging": { - "Enabled": False, - "IncludeCookies": False, - "Bucket": "", - "Prefix": "", - }, + "Logging": {"Enabled": False, "IncludeCookies": False, "Bucket": "", "Prefix": ""}, "Restrictions": {"GeoRestriction": {"RestrictionType": "none", **NULL_CONFIG}}, "WebACLId": "", } @@ -3144,8 +3091,7 @@ def get_api_id(self, lambda_name: str, apigateway_version: str = DEFAULT_APIGATE if item["name"] == lambda_name: return item["id"] - logger.exception(f"Could not get API ID. {lambda_name} {self.boto_session.region_name}") - logger.exception(response) + logger.exception("Could not get API ID.") return None except Exception: # pragma: no cover # We don't even have an API deployed. That's okay! @@ -3183,19 +3129,17 @@ def create_domain_name( certificateName=certificate_name, certificateArn=certificate_arn, ) - api_id = self.get_api_id(lambda_name) - if not api_id: - raise LookupError("No API URL to certify found - did you deploy?") - self.apigateway_client.create_base_path_mapping( - domainName=domain_name, - basePath="" if base_path is None else base_path, - restApiId=api_id, - stage=stage, - ) + api_id = self.get_api_id(lambda_name) + if not api_id: + raise LookupError("No API URL to certify found - did you deploy?") - if self.function_url_enabled: - pass + self.apigateway_client.create_base_path_mapping( + domainName=domain_name, + basePath="" if base_path is None else base_path, + restApiId=api_id, + stage=stage, + ) return agw_response["distributionDomainName"] @@ -3227,7 +3171,11 @@ def update_route53_records(self, domain_name, dns_name): # Related: https://github.com/boto/boto3/issues/157 # and: http://docs.aws.amazon.com/Route53/latest/APIReference/CreateAliasRRSAPI.html # and policy: https://spin.atomicobject.com/2016/04/28/route-53-hosted-zone-managment/ + # pure_zone_id = zone_id.split('/hostedzone/')[1] + # XXX: ClientError: An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: + # Tried to create an alias that targets d1awfeji80d0k2.cloudfront.net., type A in zone Z1XWOQP59BYF6Z, + # but the alias target name does not lie within the target zone response = self.route53.change_resource_record_sets( HostedZoneId=zone_id, ChangeBatch={"Changes": [{"Action": "UPSERT", "ResourceRecordSet": record_set}]}, @@ -3282,8 +3230,6 @@ def update_domain_name( stage=None, route53=True, base_path=None, - use_apigateway=True, - use_function_url=False, ): """ This updates your certificate information for an existing domain, @@ -3505,7 +3451,7 @@ def create_event_permission(self, lambda_name, principal, source_arn): permission_response = self.lambda_client.add_permission( FunctionName=lambda_name, - StatementId="zappa-" + "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)), + StatementId="".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)), Action="lambda:InvokeFunction", Principal=principal, SourceArn=source_arn, @@ -3536,17 +3482,18 @@ def schedule_events(self, lambda_arn, lambda_name, events, default=True): # and do not require event permissions. They do require additional permissions on the Lambda roles though. # http://docs.aws.amazon.com/lambda/latest/dg/lambda-api-permissions-ref.html pull_services = ["dynamodb", "kinesis", "sqs"] + + # XXX: Not available in Lambda yet. + # We probably want to execute the latest code. + # if default: + # lambda_arn = lambda_arn + ":$LATEST" + self.unschedule_events( lambda_name=lambda_name, lambda_arn=lambda_arn, events=events, excluded_source_services=pull_services, ) - # XXX: Not available in Lambda yet. - # We probably want to execute the latest code. - # if default: - # lambda_arn = lambda_arn + ":$LATEST" - for event in events: function = event["function"] expression = event.get("expression", None) # single expression @@ -4047,10 +3994,11 @@ def load_credentials(self, boto_session=None, profile_name=None): if profile_name: self.boto_session = boto3.Session(profile_name=profile_name, region_name=self.aws_region) elif os.environ.get("AWS_ACCESS_KEY_ID") and os.environ.get("AWS_SECRET_ACCESS_KEY"): + region_name = os.environ.get("AWS_DEFAULT_REGION") or self.aws_region session_kw = { "aws_access_key_id": os.environ.get("AWS_ACCESS_KEY_ID"), "aws_secret_access_key": os.environ.get("AWS_SECRET_ACCESS_KEY"), - "region_name": self.aws_region, + "region_name": region_name, } # If we're executing in a role, AWS_SESSION_TOKEN will be present, too. diff --git a/zappa/handler.py b/zappa/handler.py index 03fca23a3..19d61c967 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -526,19 +526,6 @@ def handler(self, event, context): try: time_start = datetime.datetime.now() - script_name = "" - host = event.get("headers", {}).get("host") - if host: - if "amazonaws.com" in host: - logger.debug("amazonaws found in host") - # The path provided in th event doesn't include the - # stage, so we must tell Flask to include the API - # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 - script_name = f"/{settings.API_STAGE}" - # fix function url domain - if host.find("lambda-url") > -1 and event.get("headers", {}).get("cloudfront-host"): - # https://stackoverflow.com/questions/73024633/cloudfront-forward-host-header-to-lambda-function-url-origin - event["headers"]["host"] = event["headers"]["cloudfront-host"] # Determine if this is API Gateway v2 (has stage) or Function URL (no stage) # API Gateway v2 includes stage in requestContext request_context = event.get("requestContext", {}) @@ -550,7 +537,7 @@ def handler(self, event, context): if stage: # API Gateway v2 with named stage - rawPath includes the stage script_name = f"/{stage}" - if host.find("lambda-url") > -1: + else: # Function URL - no stage script_name = "" diff --git a/zappa/utilities.py b/zappa/utilities.py index cb7573191..37a50426f 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -557,10 +557,7 @@ def remove(self, function_arn: str) -> bool: # Only remove Lambda permission if we actually had a subscription if subscription_removed: try: - self._lambda.remove_permission( - FunctionName=function_arn, - StatementId=f"sns-{self.arn.split(':')[-1]}", - ) + self._lambda.remove_permission(FunctionName=function_arn, StatementId=f"sns-{self.arn.split(':')[-1]}") except Exception as e: LOG.warning(f"Failed to remove Lambda permission for SNS event source {self.arn}: {e.args}") @@ -686,11 +683,7 @@ def update(self, function_arn: str) -> None: def get_event_source( - event_source: Dict[str, Any], - lambda_arn: str, - target_function: str, - boto_session: boto3.Session, - dry: bool = False, + event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False ) -> Tuple[BaseEventSource, str]: """ Given an event_source dictionary item, a session and a lambda_arn, @@ -720,11 +713,7 @@ def get_event_source( def add_event_source( - event_source: Dict[str, Any], - lambda_arn: str, - target_function: str, - boto_session: boto3.Session, - dry: bool = False, + event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False ) -> str: """ Given an event_source dictionary, create the object and add the event source. @@ -742,11 +731,7 @@ def add_event_source( def remove_event_source( - event_source: Dict[str, Any], - lambda_arn: str, - target_function: str, - boto_session: boto3.Session, - dry: bool = False, + event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False ) -> Union[BaseEventSource, bool, Dict[str, Any], None]: """ Given an event_source dictionary, create the object and remove the event source. @@ -761,11 +746,7 @@ def remove_event_source( def get_event_source_status( - event_source: Dict[str, Any], - lambda_arn: str, - target_function: str, - boto_session: boto3.Session, - dry: bool = False, + event_source: Dict[str, Any], lambda_arn: str, target_function: str, boto_session: boto3.Session, dry: bool = False ) -> Optional[Dict[str, Any]]: """ Given an event_source dictionary, create the object and get the event source status. From 8e794664c9c3b60093e32460f6971bd645ac1d21 Mon Sep 17 00:00:00 2001 From: sha Date: Tue, 24 Feb 2026 23:02:37 +0800 Subject: [PATCH 111/111] invoke managed capacity doesn't return log --- zappa/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zappa/cli.py b/zappa/cli.py index 53683b1e8..9250794ca 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -1565,6 +1565,7 @@ def invoke(self, function_name, raw_python=False, command=None, no_color=False, invocation_type="RequestResponse", client_context=client_context, qualifier=qualifier, + log_type="Tail" if not self.capacity_provider_config else "None", ) print(self.format_lambda_response(response, not no_color))