From 5b695e7504e3b9d64177510c9f01b6cd9b0af860 Mon Sep 17 00:00:00 2001 From: ian-whitestone Date: Sun, 18 Apr 2021 11:59:08 -0400 Subject: [PATCH] Update/deploy a lambda using a Docker container image Part of #922 --- .../lambda.GetFunctionConfiguration_1.json | 3 +- .../lambda.ListVersionsByFunction_2.json | 9 +- .../lambda.ListVersionsByFunction_4.json | 9 +- .../lambda.CreateAlias_1.json | 9 + .../lambda.CreateFunction_1.json | 20 + .../lambda.DeleteFunctionConcurrency_1.json | 9 + .../lambda.GetAlias_1.json | 9 + .../lambda.UpdateAlias_1.json | 9 + .../lambda.UpdateFunctionCode_1.json | 20 + .../lambda.GetFunction_1.json | 29 ++ .../lambda.ListVersionsByFunction_1.json | 27 +- .../lambda.ListVersionsByFunction_2.json | 27 +- .../lambda.ListVersionsByFunction_1.json | 128 +++++ .../lambda.ListVersionsByFunction_2.json | 128 +++++ tests/tests.py | 27 +- tests/tests_placebo.py | 39 ++ zappa/cli.py | 480 ++++++++++-------- zappa/core.py | 130 +++-- 18 files changed, 805 insertions(+), 307 deletions(-) create mode 100644 tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateAlias_1.json create mode 100644 tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateFunction_1.json create mode 100644 tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.DeleteFunctionConcurrency_1.json create mode 100644 tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.GetAlias_1.json create mode 100644 tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateAlias_1.json create mode 100644 tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateFunctionCode_1.json create mode 100644 tests/placebo/TestZappa.test_is_lambda_function_ready/lambda.GetFunction_1.json create mode 100644 tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_1.json create mode 100644 tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_2.json diff --git a/tests/placebo/TestZappa.test_cli_aws/lambda.GetFunctionConfiguration_1.json b/tests/placebo/TestZappa.test_cli_aws/lambda.GetFunctionConfiguration_1.json index bac7a999f..1d826bbf1 100644 --- a/tests/placebo/TestZappa.test_cli_aws/lambda.GetFunctionConfiguration_1.json +++ b/tests/placebo/TestZappa.test_cli_aws/lambda.GetFunctionConfiguration_1.json @@ -27,6 +27,7 @@ "Version": "$LATEST", "Environment": { "Variables": {} - } + }, + "PackageType": "Zip" } } \ No newline at end of file diff --git a/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_2.json b/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_2.json index f939d9acc..463721dd5 100644 --- a/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_2.json +++ b/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_2.json @@ -18,7 +18,8 @@ "Timeout": 30, "LastModified": "2016-06-02T19:24:32.878+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "1", @@ -32,7 +33,8 @@ "Timeout": 30, "LastModified": "2016-06-02T19:23:48.902+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "2", @@ -46,7 +48,8 @@ "Timeout": 30, "LastModified": "2016-06-02T19:24:32.878+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" } ] } diff --git a/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_4.json b/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_4.json index f0241afd6..e8b68dc6a 100644 --- a/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_4.json +++ b/tests/placebo/TestZappa.test_cli_aws/lambda.ListVersionsByFunction_4.json @@ -25,7 +25,8 @@ "Timeout": 30, "LastModified": "2016-08-25T03:31:23.343+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "4", @@ -39,7 +40,8 @@ "Timeout": 30, "LastModified": "2016-08-25T03:29:40.612+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "5", @@ -53,7 +55,8 @@ "Timeout": 30, "LastModified": "2016-08-25T03:31:18.572+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" } ] } diff --git a/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateAlias_1.json b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateAlias_1.json new file mode 100644 index 000000000..08d0bb9ed --- /dev/null +++ b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateAlias_1.json @@ -0,0 +1,9 @@ +{ + "status_code": 201, + "data": { + "AliasArn": "arn:aws:lambda:us-east-1:12345:function:test_lmbda_function55:current-alb-version", + "Description": "Zappa Deployment", + "FunctionVersion": "1", + "Name": "current-alb-version" + } +} diff --git a/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateFunction_1.json b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateFunction_1.json new file mode 100644 index 000000000..61a6508a0 --- /dev/null +++ b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.CreateFunction_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 201, + "data": { + "CodeSha256": "q4duEOI611sqtkU+YbdNkjH5qGRlgmvc9+FhpdykYuk=", + "FunctionName": "test_lmbda_function55", + "ResponseMetadata": { + "HTTPStatusCode": 201, + "RequestId": "12b75ef1-e226-11e5-84a2-ad7ddc64ad40" + }, + "CodeSize": 26585626, + "MemorySize": 512, + "FunctionArn": "arn:aws:lambda:us-east-1:12345:function:test_lmbda_function55", + "Version": "1", + "Role": "arn:aws:iam::12345:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-04T16:28:06.633+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + } +} diff --git a/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.DeleteFunctionConcurrency_1.json b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.DeleteFunctionConcurrency_1.json new file mode 100644 index 000000000..c226aef56 --- /dev/null +++ b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.DeleteFunctionConcurrency_1.json @@ -0,0 +1,9 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": { + "HTTPStatusCode": 200, + "RequestId": "aff3a3f9-28f4-11e6-9dbb-5dd116b9ddf1" + } + } +} diff --git a/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.GetAlias_1.json b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.GetAlias_1.json new file mode 100644 index 000000000..cf839c230 --- /dev/null +++ b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.GetAlias_1.json @@ -0,0 +1,9 @@ +{ + "status_code": 200, + "data": { + "AliasArn": "arn:aws:lambda:us-east-1:12345:function:test_lmbda_function55:current-alb-version", + "Description": "Zappa Deployment", + "FunctionVersion": "1", + "Name": "current-alb-version" + } +} diff --git a/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateAlias_1.json b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateAlias_1.json new file mode 100644 index 000000000..08d0bb9ed --- /dev/null +++ b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateAlias_1.json @@ -0,0 +1,9 @@ +{ + "status_code": 201, + "data": { + "AliasArn": "arn:aws:lambda:us-east-1:12345:function:test_lmbda_function55:current-alb-version", + "Description": "Zappa Deployment", + "FunctionVersion": "1", + "Name": "current-alb-version" + } +} diff --git a/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateFunctionCode_1.json b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateFunctionCode_1.json new file mode 100644 index 000000000..fdb149ba8 --- /dev/null +++ b/tests/placebo/TestZappa.test_create_lambda_function_docker/lambda.UpdateFunctionCode_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "CodeSha256": "q4duEOI611sqtkU+YbdNkjH5qGRlgmvc9+FhpdykYuk=", + "FunctionName": "test_lmbda_function55", + "ResponseMetadata": { + "HTTPStatusCode": 200, + "RequestId": "153e09be-e226-11e5-a3b8-7b263f053e5a" + }, + "CodeSize": 26585626, + "MemorySize": 512, + "FunctionArn": "arn:aws:lambda:us-east-1:12345:function:test_lmbda_function55:1", + "Version": "1", + "Role": "arn:aws:iam::12345:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-04T16:28:06.633+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + } +} diff --git a/tests/placebo/TestZappa.test_is_lambda_function_ready/lambda.GetFunction_1.json b/tests/placebo/TestZappa.test_is_lambda_function_ready/lambda.GetFunction_1.json new file mode 100644 index 000000000..7debf4b45 --- /dev/null +++ b/tests/placebo/TestZappa.test_is_lambda_function_ready/lambda.GetFunction_1.json @@ -0,0 +1,29 @@ +{ + "status_code": 200, + "data": { + "Code": { + "RepositoryType": "S3", + "Location": "https://prod-04-2014-tasks.s3.amazonaws.com/snapshots/724336686645/django-helloworld-unicode-a0d56d69-de7c-421b-9527-f49e100a8613?x-amz-security-token=AQoDYXdzEEka4AOil22HD1z9lkxZ7yiced%2FbYEWzXJHbMHexq2zxzMQ7%2Fj2a06AP9z3nK0QgGPUADK2A6FUpFFl%2BjO7gBmP%2FKifl9jTvvaf72YhPbZDJTIrFvZZ%2B5NjhkDrfRgyK%2BhBROGNH85L46iJSShwZ5lmKgADTnnVMT9pZ2JXF3uNLUzrWoJTpf%2F7lDBMEoFO%2BNLer7MsLsuCiemzKV4Kcvo0Mu3qhkk1u2BfoRTj4xmiaWE7UfqD%2FeQkINdwkUgXeQ9SA1T2l70omZ1ss2l1y18xeUvCQde5OrE7ue5cVpENyjYr9dEwPYwm1wG9%2B%2FJE%2BMQZ8Lz6CNex%2BTVuBvY09JL8vTV7WpRxGhr96a7OIn5YCCQ7cJfNVIWpdBUrWmqnE8Jh9oL558TVA0e5RFP9YU%2BFYxJcA6fH%2B0DZBAAK9797XPltzhSXrUcB8JuHfa%2FC1n2w8X7HbsMTI8m1OAIkQQtppYHtl4T5o3012VcSTLRSv945sCodMOmueKsEwAZlc1gFflcwGXE4v4hB6YJjvcRj2SQiK7Yzw7ShODOroOsWT18te8p9Qjs2mL6akLt1%2Fo8tddWskKzqrfYfBTvfFL5Y0nU4sI96JUXM0KXmJotPRmCdFEktGk4Fre0wm%2FyAau0NdaDQg7NzmtgU%3D&AWSAccessKeyId=ASIAIQFLXUK7E7GUTQ2A&Expires=1457113092&Signature=%2FH0RMGKWk1tEGnK9R6CCPXQ6Y04%3D" + }, + "Configuration": { + "Version": "7", + "CodeSha256": "t3pcqjdrLOqd2p0bRsCEOhgvLJ9sLJrVazpGqhDybsc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154784, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:7", + "Handler": "handler.lambda_handler", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:24:01.446+0000", + "Runtime": "python2.7", + "Description": "Zappa Deployment", + "State": "Active", + "LastUpdateStatus": "Successful" + }, + "ResponseMetadata": { + "HTTPStatusCode": 200, + "RequestId": "78da1863-e22e-11e5-a5ae-01cbfdebdc8f" + } + } +} diff --git a/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_1.json b/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_1.json index d88e58f13..9dcb16e68 100644 --- a/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_1.json +++ b/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_1.json @@ -18,7 +18,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:55:35.845+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "1", @@ -32,7 +33,8 @@ "Timeout": 30, "LastModified": "2016-02-29T14:39:31.557+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "2", @@ -46,7 +48,8 @@ "Timeout": 30, "LastModified": "2016-02-29T14:47:04.499+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "3", @@ -60,7 +63,8 @@ "Timeout": 30, "LastModified": "2016-02-29T14:58:08.443+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "4", @@ -74,7 +78,8 @@ "Timeout": 30, "LastModified": "2016-02-29T16:25:49.425+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "5", @@ -88,7 +93,8 @@ "Timeout": 30, "LastModified": "2016-02-29T16:34:47.988+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "6", @@ -102,7 +108,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:12:28.708+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "7", @@ -116,7 +123,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:24:01.446+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "8", @@ -130,7 +138,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:55:35.845+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" } ] } diff --git a/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_2.json b/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_2.json index 0970214cb..5ce591a50 100644 --- a/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_2.json +++ b/tests/placebo/TestZappa.test_rollback_lambda_function_version/lambda.ListVersionsByFunction_2.json @@ -18,7 +18,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:55:35.845+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "1", @@ -32,7 +33,8 @@ "Timeout": 30, "LastModified": "2016-02-29T14:39:31.557+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "2", @@ -46,7 +48,8 @@ "Timeout": 30, "LastModified": "2016-02-29T14:47:04.499+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "3", @@ -60,7 +63,8 @@ "Timeout": 30, "LastModified": "2016-02-29T14:58:08.443+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "4", @@ -74,7 +78,8 @@ "Timeout": 30, "LastModified": "2016-02-29T16:25:49.425+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "5", @@ -88,7 +93,8 @@ "Timeout": 30, "LastModified": "2016-02-29T16:34:47.988+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "6", @@ -102,7 +108,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:12:28.708+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "7", @@ -116,7 +123,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:24:01.446+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" }, { "Version": "8", @@ -130,7 +138,8 @@ "Timeout": 30, "LastModified": "2016-03-01T00:55:35.845+0000", "Runtime": "python2.7", - "Description": "Zappa Deployment" + "Description": "Zappa Deployment", + "PackageType": "Zip" } ] } diff --git a/tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_1.json b/tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_1.json new file mode 100644 index 000000000..1e4ff953f --- /dev/null +++ b/tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_1.json @@ -0,0 +1,128 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": { + "HTTPStatusCode": 200, + "RequestId": "786c144e-e22e-11e5-86f4-4528d3e5269b" + }, + "Versions": [ + { + "Version": "$LATEST", + "CodeSha256": "qPyTWxngvz505vUX4v0IbW4H6CdQCSiYSGPsNQHPNTc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154778, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:$LATEST", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:55:35.845+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "1", + "CodeSha256": "eEW6OoZIm1g+3sLnoGqkfG9PcvYcXYoTd8uZYCMhA2U=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12102921, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:1", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T14:39:31.557+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "2", + "CodeSha256": "QXwkHDWCmczXTdDi9AYf6Ws74jkAI6zUJ+tf7s5zLzk=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12152312, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:2", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T14:47:04.499+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "3", + "CodeSha256": "Hpt/J6gyhnYXm7NVWwzomJoExr0i9II//4zaLm0DYgc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12152182, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:3", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T14:58:08.443+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "4", + "CodeSha256": "1ofpfK2Jonu13iPUBMeNYR962iWcD5vL1YrJazF9tF0=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154467, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:4", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T16:25:49.425+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "5", + "CodeSha256": "bwLhQSjeZOmNC10s2eMO0ljWJBNX5tT4S9MgkvBWjOM=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154507, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:5", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T16:34:47.988+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "6", + "CodeSha256": "uxHYC6Uf/sUl884TWUqlwi0WWE+ixfxhO964yPec5zM=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154780, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:6", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:12:28.708+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "7", + "CodeSha256": "t3pcqjdrLOqd2p0bRsCEOhgvLJ9sLJrVazpGqhDybsc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154784, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:7", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:24:01.446+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "8", + "CodeSha256": "qPyTWxngvz505vUX4v0IbW4H6CdQCSiYSGPsNQHPNTc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154778, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:8", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:55:35.845+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + } + ] + } +} diff --git a/tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_2.json b/tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_2.json new file mode 100644 index 000000000..cfe367df5 --- /dev/null +++ b/tests/placebo/TestZappa.test_rollback_lambda_function_version_docker/lambda.ListVersionsByFunction_2.json @@ -0,0 +1,128 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": { + "HTTPStatusCode": 200, + "RequestId": "78c11231-e22e-11e5-bb00-6935e1456f05" + }, + "Versions": [ + { + "Version": "$LATEST", + "CodeSha256": "qPyTWxngvz505vUX4v0IbW4H6CdQCSiYSGPsNQHPNTc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154778, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:$LATEST", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:55:35.845+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "1", + "CodeSha256": "eEW6OoZIm1g+3sLnoGqkfG9PcvYcXYoTd8uZYCMhA2U=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12102921, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:1", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T14:39:31.557+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "2", + "CodeSha256": "QXwkHDWCmczXTdDi9AYf6Ws74jkAI6zUJ+tf7s5zLzk=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12152312, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:2", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T14:47:04.499+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "3", + "CodeSha256": "Hpt/J6gyhnYXm7NVWwzomJoExr0i9II//4zaLm0DYgc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12152182, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:3", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T14:58:08.443+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "4", + "CodeSha256": "1ofpfK2Jonu13iPUBMeNYR962iWcD5vL1YrJazF9tF0=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154467, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:4", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T16:25:49.425+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "5", + "CodeSha256": "bwLhQSjeZOmNC10s2eMO0ljWJBNX5tT4S9MgkvBWjOM=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154507, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:5", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-02-29T16:34:47.988+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "6", + "CodeSha256": "uxHYC6Uf/sUl884TWUqlwi0WWE+ixfxhO964yPec5zM=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154780, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:6", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:12:28.708+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "7", + "CodeSha256": "t3pcqjdrLOqd2p0bRsCEOhgvLJ9sLJrVazpGqhDybsc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154784, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:7", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:24:01.446+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + }, + { + "Version": "8", + "CodeSha256": "qPyTWxngvz505vUX4v0IbW4H6CdQCSiYSGPsNQHPNTc=", + "FunctionName": "django-helloworld-unicode", + "MemorySize": 512, + "CodeSize": 12154778, + "FunctionArn": "arn:aws:lambda:us-east-1:724336686645:function:django-helloworld-unicode:8", + "Role": "arn:aws:iam::724336686645:role/ZappaLambdaExecution", + "Timeout": 30, + "LastModified": "2016-03-01T00:55:35.845+0000", + "Description": "Zappa Deployment", + "PackageType": "Image" + } + ] + } +} diff --git a/tests/tests.py b/tests/tests.py index dff19705b..5b35e19e2 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -578,9 +578,10 @@ def test_update_aws_env_vars(self): with mock.patch.object(z, "lambda_client") as mock_client: # Simulate already having some AWS env vars remotely mock_client.get_function_configuration.return_value = { + "PackageType": "Zip", "Environment": { "Variables": {"REMOTE_ONLY": "AAA", "CHANGED_REMOTE": "BBB"} - } + }, } z.update_lambda_configuration( "test", @@ -601,9 +602,10 @@ def test_update_aws_env_vars(self): with mock.patch.object(z, "lambda_client") as mock_client: # Simulate already having some AWS env vars remotely but none set in aws_environment_variables mock_client.get_function_configuration.return_value = { + "PackageType": "Zip", "Environment": { "Variables": {"REMOTE_ONLY_1": "AAA", "REMOTE_ONLY_2": "BBB"} - } + }, } z.update_lambda_configuration("test", "test", "test") end_result_should_be = {"REMOTE_ONLY_1": "AAA", "REMOTE_ONLY_2": "BBB"} @@ -617,7 +619,7 @@ def test_update_layers(self): z.credentials_arn = object() with mock.patch.object(z, "lambda_client") as mock_client: - mock_client.get_function_configuration.return_value = {} + mock_client.get_function_configuration.return_value = {"PackageType": "Zip"} z.update_lambda_configuration( "test", "test", "test", layers=["Layer1", "Layer2"] ) @@ -626,7 +628,7 @@ def test_update_layers(self): ["Layer1", "Layer2"], ) with mock.patch.object(z, "lambda_client") as mock_client: - mock_client.get_function_configuration.return_value = {} + mock_client.get_function_configuration.return_value = {"PackageType": "Zip"} z.update_lambda_configuration("test", "test", "test") self.assertEqual( mock_client.update_function_configuration.call_args[1]["Layers"], [] @@ -638,7 +640,7 @@ def test_update_empty_aws_env_hash(self): with mock.patch.object(z, "lambda_client") as mock_client: # Simulate having no AWS env vars remotely - mock_client.get_function_configuration.return_value = {} + mock_client.get_function_configuration.return_value = {"PackageType": "Zip"} z.update_lambda_configuration( "test", "test", @@ -1253,6 +1255,21 @@ def test_cli_colorize_invoke_command_bad_string(self): colorized_string = zappa_cli.colorize_invoke_command(plain_string) self.assertEqual(final_string, colorized_string) + def test_cli_save_python_settings_file(self): + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "ttt888" + zappa_cli.load_settings("test_settings.json") + + temp_dir = tempfile.mkdtemp() + good_output_path = os.path.join(temp_dir, "zappa_settings.py") + assert not os.path.exists(good_output_path) + zappa_cli.save_python_settings_file(good_output_path) + assert os.path.exists(good_output_path) + + bad_output_path = os.path.join(temp_dir, "settings.py") + with self.assertRaises(ValueError): + zappa_cli.save_python_settings_file(bad_output_path) + # def test_cli_args(self): # zappa_cli = ZappaCLI() # # Sanity diff --git a/tests/tests_placebo.py b/tests/tests_placebo.py index c203509b0..8836e18e1 100644 --- a/tests/tests_placebo.py +++ b/tests/tests_placebo.py @@ -128,6 +128,28 @@ def test_create_lambda_function_local(self, session): function_name="test_lmbda_function55", ) + @placebo_session + def test_create_lambda_function_docker(self, session): + bucket_name = "lmbda" + docker_image_uri = "docker_image_uri" + + z = Zappa(session) + z.aws_region = "us-east-1" + z.load_credentials(session) + z.credentials_arn = "arn:aws:iam::12345:role/ZappaLambdaExecution" + + arn = z.create_lambda_function( + bucket=bucket_name, + docker_image_uri=docker_image_uri, + function_name="test_lmbda_function55", + ) + + arn = z.update_lambda_function( + bucket=bucket_name, + docker_image_uri=docker_image_uri, + function_name="test_lmbda_function55", + ) + @placebo_session def test_rollback_lambda_function_version(self, session): z = Zappa(session) @@ -139,6 +161,23 @@ def test_rollback_lambda_function_version(self, session): function_arn = z.rollback_lambda_function_version(function_name, 1) + @placebo_session + def test_rollback_lambda_function_version_docker(self, session): + z = Zappa(session) + z.credentials_arn = "arn:aws:iam::724336686645:role/ZappaLambdaExecution" + + function_name = "django-helloworld-unicode" + + with self.assertRaises(NotImplementedError): + z.rollback_lambda_function_version(function_name) + + @placebo_session + def test_is_lambda_function_ready(self, session): + z = Zappa(session) + z.credentials_arn = "arn:aws:iam::724336686645:role/ZappaLambdaExecution" + function_name = "django-helloworld-unicode" + z.is_lambda_function_ready(function_name) + @placebo_session def test_invoke_lambda_function(self, session): z = Zappa(session) diff --git a/zappa/cli.py b/zappa/cli.py index 9eb6a00a8..6ba6dd5fe 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -83,7 +83,6 @@ class ZappaCLI: """ ZappaCLI object is responsible for loading the settings, handling the input arguments and executing the calls to the core library. - """ # CLI @@ -198,9 +197,7 @@ def override_stage_config_setting(self, key, val): def handle(self, argv=None): """ Main function. - Parses command, load settings and dispatches accordingly. - """ desc = "Zappa - Deploy Python applications to AWS Lambda" " and API Gateway.\n" @@ -277,6 +274,11 @@ def handle(self, argv=None): "--zip", help="Deploy Lambda with specific local or S3 hosted zip package", ) + deploy_parser.add_argument( + "-d", + "--docker-image-uri", + help="Deploy Lambda with a specific docker image hosted in AWS Elastic Container Registry", + ) ## # Init @@ -478,6 +480,11 @@ def positive_int(s): "--no-upload", help="Update configuration where appropriate, but don't upload new code", ) + update_parser.add_argument( + "-d", + "--docker-image-uri", + help="Update Lambda with a specific docker image hosted in AWS Elastic Container Registry", + ) ## # Debug @@ -488,6 +495,24 @@ def positive_int(s): help="A debug shell with a loaded Zappa object.", ) + ## + # Python Settings File + ## + settings_parser = subparsers.add_parser( + "save-python-settings-file", + parents=[env_parser], + help="Generate & save the Zappa settings Python file for docker deployments", + ) + settings_parser.add_argument( + "-o", + "--output_path", + help=( + "The path to save the Zappa settings Python file. " + "File must be named zappa_settings.py and should be saved " + "in the same directory as the Zappa handler.py" + ), + ) + argcomplete.autocomplete(parser) args = parser.parse_args(argv) self.vargs = vars(args) @@ -602,7 +627,7 @@ def dispatch_command(self, command, stage): # Hand it off if command == "deploy": # pragma: no cover - self.deploy(self.vargs["zip"]) + self.deploy(self.vargs["zip"], self.vargs["docker_image_uri"]) if command == "package": # pragma: no cover self.package(self.vargs["output"]) if command == "template": # pragma: no cover @@ -613,7 +638,11 @@ def dispatch_command(self, command, stage): json=self.vargs["json"], ) elif command == "update": # pragma: no cover - self.update(self.vargs["zip"], self.vargs["no_upload"]) + self.update( + self.vargs["zip"], + self.vargs["no_upload"], + self.vargs["docker_image_uri"], + ) elif command == "rollback": # pragma: no cover self.rollback(self.vargs["num_rollback"]) elif command == "invoke": # pragma: no cover @@ -676,11 +705,26 @@ def dispatch_command(self, command, stage): self.certify(no_confirm=self.vargs["yes"], manual=self.vargs["manual"]) elif command == "shell": # pragma: no cover self.shell() + elif command == "save-python-settings-file": # pragma: no cover + self.save_python_settings_file(self.vargs["output_path"]) ## # The Commands ## + def save_python_settings_file(self, output_path=None): + settings_path = output_path or "zappa_settings.py" + print( + "Generating Zappa settings Python file and saving to {}".format( + settings_path + ) + ) + if not settings_path.endswith("zappa_settings.py"): + raise ValueError("Settings file must be named zappa_settings.py") + zappa_settings_s = self.get_zappa_settings_string() + with open(settings_path, "w") as f_out: + f_out.write(zappa_settings_s) + def package(self, output=None): """ Only build the package @@ -752,34 +796,13 @@ def template(self, lambda_arn, role_arn, output=None, json=False): with open(template_file, "r") as out: print(out.read()) - def deploy(self, source_zip=None): + def deploy(self, source_zip=None, docker_image_uri=None): """ Package your project, upload it to S3, register the Lambda function and create the API Gateway routes. - """ - if not source_zip: - # Make sure we're in a venv. - self.check_venv() - - # Execute the prebuild script - if self.prebuild_script: - self.execute_prebuild_script() - - # Make sure this isn't already deployed. - deployed_versions = self.zappa.get_lambda_function_versions( - self.lambda_name - ) - if len(deployed_versions) > 0: - raise ClickException( - "This application is " - + click.style("already deployed", fg="red") - + " - did you mean to call " - + click.style("update", bold=True) - + "?" - ) - + if not source_zip or docker_image_uri: # Make sure the necessary IAM execution roles are available if self.manage_roles: try: @@ -804,6 +827,25 @@ def deploy(self, source_zip=None): + "\n" ) + # Make sure this isn't already deployed. + deployed_versions = self.zappa.get_lambda_function_versions(self.lambda_name) + if len(deployed_versions) > 0: + raise ClickException( + "This application is " + + click.style("already deployed", fg="red") + + " - did you mean to call " + + click.style("update", bold=True) + + "?" + ) + + if not source_zip and not docker_image_uri: + # Make sure we're in a venv. + self.check_venv() + + # Execute the prebuild script + if self.prebuild_script: + self.execute_prebuild_script() + # Create the Lambda Zip self.create_package() self.callback("zip") @@ -868,18 +910,18 @@ def deploy(self, source_zip=None): layers=self.layers, concurrency=self.lambda_concurrency, ) - if source_zip and source_zip.startswith("s3://"): + kwargs["function_name"] = self.lambda_name + if docker_image_uri: + kwargs["docker_image_uri"] = docker_image_uri + elif source_zip and source_zip.startswith("s3://"): bucket, key_name = parse_s3_url(source_zip) - kwargs["function_name"] = self.lambda_name kwargs["bucket"] = bucket kwargs["s3_key"] = key_name elif source_zip and not source_zip.startswith("s3://"): with open(source_zip, mode="rb") as fh: byte_stream = fh.read() - kwargs["function_name"] = self.lambda_name kwargs["local_zip"] = byte_stream else: - kwargs["function_name"] = self.lambda_name kwargs["bucket"] = self.s3_bucket_name kwargs["s3_key"] = handler_file @@ -952,27 +994,30 @@ def deploy(self, source_zip=None): ) if self.stage_config.get("touch", True): + self.zappa.wait_until_lambda_function_is_ready( + function_name=self.lambda_name + ) self.touch_endpoint(endpoint_url) # Finally, delete the local copy our zip package - if not source_zip: + if not source_zip and not docker_image_uri: if self.stage_config.get("delete_local_zip", True): self.remove_local_zip() # Remove the project zip from S3. - if not source_zip: + if not source_zip and not docker_image_uri: self.remove_uploaded_zip() self.callback("post") click.echo(deployment_string) - def update(self, source_zip=None, no_upload=False): + def update(self, source_zip=None, no_upload=False, docker_image_uri=None): """ Repackage and update the function code. """ - if not source_zip: + if not source_zip and not docker_image_uri: # Make sure we're in a venv. self.check_venv() @@ -1090,7 +1135,13 @@ def update(self, source_zip=None, no_upload=False): num_revisions=self.num_retained_versions, concurrency=self.lambda_concurrency, ) - if source_zip and source_zip.startswith("s3://"): + if docker_image_uri: + kwargs["docker_image_uri"] = docker_image_uri + self.lambda_arn = self.zappa.update_lambda_function(**kwargs) + self.zappa.wait_until_lambda_function_is_ready( + function_name=self.lambda_name + ) + elif source_zip and source_zip.startswith("s3://"): bucket, key_name = parse_s3_url(source_zip) kwargs.update(dict(bucket=bucket, s3_key=key_name)) self.lambda_arn = self.zappa.update_lambda_function(**kwargs) @@ -1105,7 +1156,7 @@ def update(self, source_zip=None, no_upload=False): self.lambda_arn = self.zappa.update_lambda_function(**kwargs) # Remove the uploaded zip from S3, because it is now registered.. - if not source_zip and not no_upload: + if not source_zip and not no_upload and not docker_image_uri: self.remove_uploaded_zip() # Update the configuration, in case there are changes. @@ -1124,7 +1175,7 @@ def update(self, source_zip=None, no_upload=False): ) # Finally, delete the local copy our zip package - if not source_zip and not no_upload: + if not source_zip and not no_upload and not docker_image_uri: if self.stage_config.get("delete_local_zip", True): self.remove_local_zip() @@ -1210,6 +1261,9 @@ def update(self, source_zip=None, no_upload=False): deployed_string = deployed_string + " (" + api_url + ")" if self.stage_config.get("touch", True): + self.zappa.wait_until_lambda_function_is_ready( + function_name=self.lambda_name + ) if api_url: self.touch_endpoint(api_url) elif endpoint_url: @@ -1242,7 +1296,6 @@ def tail( ): """ Tail this function's logs. - if keep_open, do so repeatedly, printing any new logs """ @@ -1328,7 +1381,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", []) @@ -1422,7 +1474,6 @@ def unschedule(self): """ Given a a list of scheduled functions, tear down their regular execution. - """ # Run even if events are not defined to remove previously existing ones (thus default to []). @@ -1523,7 +1574,6 @@ def colorize_invoke_command(self, string): """ Apply various heuristics to return a colorized version the invoke command string. If these fail, simply return the string in plaintext. - Inspired by colorize_log_entry(). """ @@ -1616,13 +1666,15 @@ def tabular_print(title, value): status_dict["Lambda Name"] = self.lambda_name status_dict["Lambda ARN"] = self.lambda_arn status_dict["Lambda Role ARN"] = conf["Role"] - status_dict["Lambda Handler"] = conf["Handler"] status_dict["Lambda Code Size"] = conf["CodeSize"] status_dict["Lambda Version"] = conf["Version"] status_dict["Lambda Last Modified"] = conf["LastModified"] status_dict["Lambda Memory Size"] = conf["MemorySize"] status_dict["Lambda Timeout"] = conf["Timeout"] - status_dict["Lambda Runtime"] = conf["Runtime"] + # Handler & Runtime won't be present for lambda Docker deployments + # https://github.com/Miserlou/Zappa/issues/2188 + status_dict["Lambda Handler"] = conf.get("Handler", "") + status_dict["Lambda Runtime"] = conf.get("Runtime", "") if "VpcConfig" in conf.keys(): status_dict["Lambda VPC ID"] = conf.get("VpcConfig", {}).get( "VpcId", "Not assigned" @@ -1726,7 +1778,6 @@ def tabular_print(title, value): def check_stage_name(self, stage_name): """ Make sure the stage name matches the AWS-allowed pattern - (calls to apigateway_client.create_deployment, will fail with error message "ClientError: An error occurred (BadRequestException) when calling the CreateDeployment operation: Stage name only allows @@ -1739,7 +1790,6 @@ def check_stage_name(self, stage_name): def check_environment(self, environment): """ Make sure the environment contains only strings - (since putenv needs a string) """ @@ -1759,10 +1809,8 @@ def check_environment(self, environment): def init(self, settings_file="zappa_settings.json"): """ Initialize a new Zappa project by creating a new zappa_settings.json in a guided process. - This should probably be broken up into few separate componants once it's stable. Testing these inputs requires monkeypatching with mock, which isn't pretty. - """ # Make sure we're in a venv. @@ -2331,7 +2379,6 @@ def shell(self): def callback(self, position): """ Allows the execution of custom code between creation of the zip file and deployment to AWS. - :return: None """ @@ -2417,9 +2464,7 @@ def check_for_update(self): def load_settings(self, settings_file=None, session=None): """ Load the local zappa_settings file. - An existing boto session can be supplied, though this is likely for testing purposes. - Returns the loaded Zappa object. """ @@ -2673,7 +2718,6 @@ def create_package(self, output=None): """ Ensure that the package can be properly configured, and then create it. - """ # Create the Lambda zip package (includes project and virtualenvironment) @@ -2750,191 +2794,186 @@ def create_package(self, output=None): with zipfile.ZipFile(handler_zip, "a") as lambda_zip: - settings_s = "# Generated by Zappa\n" - - if self.app_function: - if "." not in self.app_function: # pragma: no cover - raise ClickException( - "Your " - + click.style("app_function", fg="red", bold=True) - + " value is not a modular path." - + " It needs to be in the format `" - + click.style("your_module.your_app_object", bold=True) - + "`." - ) - app_module, app_function = self.app_function.rsplit(".", 1) - settings_s = ( - settings_s - + "APP_MODULE='{0!s}'\nAPP_FUNCTION='{1!s}'\n".format( - app_module, app_function - ) - ) + settings_s = self.get_zappa_settings_string() - if self.exception_handler: - settings_s += "EXCEPTION_HANDLER='{0!s}'\n".format( - self.exception_handler - ) - else: - settings_s += "EXCEPTION_HANDLER=None\n" - - if self.debug: - settings_s = settings_s + "DEBUG=True\n" - else: - settings_s = settings_s + "DEBUG=False\n" + # Copy our Django app into root of our package. + # It doesn't work otherwise. + if self.django_settings: + base = __file__.rsplit(os.sep, 1)[0] + django_py = "".join(os.path.join(base, "ext", "django_zappa.py")) + lambda_zip.write(django_py, "django_zappa_app.py") - settings_s = settings_s + "LOG_LEVEL='{0!s}'\n".format((self.log_level)) + # Lambda requires a specific chmod + temp_settings = tempfile.NamedTemporaryFile(delete=False) + os.chmod(temp_settings.name, 0o644) + temp_settings.write(bytes(settings_s, "utf-8")) + temp_settings.close() + lambda_zip.write(temp_settings.name, "zappa_settings.py") + os.unlink(temp_settings.name) - if self.binary_support: - settings_s = settings_s + "BINARY_SUPPORT=True\n" - else: - settings_s = settings_s + "BINARY_SUPPORT=False\n" + def get_zappa_settings_string(self): + settings_s = "# Generated by Zappa\n" - head_map_dict = {} - head_map_dict.update(dict(self.context_header_mappings)) - settings_s = settings_s + "CONTEXT_HEADER_MAPPINGS={0}\n".format( - head_map_dict + if self.app_function: + if "." not in self.app_function: # pragma: no cover + raise ClickException( + "Your " + + click.style("app_function", fg="red", bold=True) + + " value is not a modular path." + + " It needs to be in the format `" + + click.style("your_module.your_app_object", bold=True) + + "`." + ) + app_module, app_function = self.app_function.rsplit(".", 1) + settings_s = ( + settings_s + + "APP_MODULE='{0!s}'\nAPP_FUNCTION='{1!s}'\n".format( + app_module, app_function + ) ) - # If we're on a domain, we don't need to define the /<> in - # the WSGI PATH - if self.domain: - settings_s = settings_s + "DOMAIN='{0!s}'\n".format((self.domain)) - else: - settings_s = settings_s + "DOMAIN=None\n" + if self.exception_handler: + settings_s += "EXCEPTION_HANDLER='{0!s}'\n".format(self.exception_handler) + else: + settings_s += "EXCEPTION_HANDLER=None\n" - if self.base_path: - settings_s = settings_s + "BASE_PATH='{0!s}'\n".format((self.base_path)) - else: - settings_s = settings_s + "BASE_PATH=None\n" - - # Pass through remote config bucket and path - if self.remote_env: - settings_s = settings_s + "REMOTE_ENV='{0!s}'\n".format(self.remote_env) - # DEPRECATED. use remove_env instead - elif self.remote_env_bucket and self.remote_env_file: - settings_s = settings_s + "REMOTE_ENV='s3://{0!s}/{1!s}'\n".format( - self.remote_env_bucket, self.remote_env_file - ) + if self.debug: + settings_s = settings_s + "DEBUG=True\n" + else: + settings_s = settings_s + "DEBUG=False\n" - # Local envs - env_dict = {} - if self.aws_region: - env_dict["AWS_REGION"] = self.aws_region - env_dict.update(dict(self.environment_variables)) + settings_s = settings_s + "LOG_LEVEL='{0!s}'\n".format((self.log_level)) - # Environment variable keys must be ascii - # https://github.com/Miserlou/Zappa/issues/604 - # https://github.com/Miserlou/Zappa/issues/998 - try: - env_dict = dict( - (k.encode("ascii").decode("ascii"), v) - for (k, v) in env_dict.items() - ) - except Exception: - raise ValueError("Environment variable keys must be ascii.") + if self.binary_support: + settings_s = settings_s + "BINARY_SUPPORT=True\n" + else: + settings_s = settings_s + "BINARY_SUPPORT=False\n" - settings_s = settings_s + "ENVIRONMENT_VARIABLES={0}\n".format(env_dict) + head_map_dict = {} + head_map_dict.update(dict(self.context_header_mappings)) + settings_s = settings_s + "CONTEXT_HEADER_MAPPINGS={0}\n".format(head_map_dict) + + # If we're on a domain, we don't need to define the /<> in + # the WSGI PATH + if self.domain: + settings_s = settings_s + "DOMAIN='{0!s}'\n".format((self.domain)) + else: + settings_s = settings_s + "DOMAIN=None\n" - # We can be environment-aware - settings_s = settings_s + "API_STAGE='{0!s}'\n".format((self.api_stage)) - settings_s = settings_s + "PROJECT_NAME='{0!s}'\n".format( - (self.project_name) + if self.base_path: + settings_s = settings_s + "BASE_PATH='{0!s}'\n".format((self.base_path)) + else: + settings_s = settings_s + "BASE_PATH=None\n" + + # Pass through remote config bucket and path + if self.remote_env: + settings_s = settings_s + "REMOTE_ENV='{0!s}'\n".format(self.remote_env) + # DEPRECATED. use remove_env instead + elif self.remote_env_bucket and self.remote_env_file: + settings_s = settings_s + "REMOTE_ENV='s3://{0!s}/{1!s}'\n".format( + self.remote_env_bucket, self.remote_env_file + ) + + # Local envs + env_dict = {} + if self.aws_region: + env_dict["AWS_REGION"] = self.aws_region + env_dict.update(dict(self.environment_variables)) + + # Environment variable keys must be ascii + # https://github.com/Miserlou/Zappa/issues/604 + # https://github.com/Miserlou/Zappa/issues/998 + try: + env_dict = dict( + (k.encode("ascii").decode("ascii"), v) for (k, v) in env_dict.items() ) + except Exception: + raise ValueError("Environment variable keys must be ascii.") - if self.settings_file: - settings_s = settings_s + "SETTINGS_FILE='{0!s}'\n".format( - (self.settings_file) - ) - else: - settings_s = settings_s + "SETTINGS_FILE=None\n" + settings_s = settings_s + "ENVIRONMENT_VARIABLES={0}\n".format(env_dict) - if self.django_settings: - settings_s = settings_s + "DJANGO_SETTINGS='{0!s}'\n".format( - (self.django_settings) - ) - else: - settings_s = settings_s + "DJANGO_SETTINGS=None\n" + # We can be environment-aware + settings_s = settings_s + "API_STAGE='{0!s}'\n".format((self.api_stage)) + settings_s = settings_s + "PROJECT_NAME='{0!s}'\n".format((self.project_name)) - # If slim handler, path to project zip - if self.stage_config.get("slim_handler", False): - settings_s += "ARCHIVE_PATH='s3://{0!s}/{1!s}_{2!s}_current_project.tar.gz'\n".format( - self.s3_bucket_name, self.api_stage, self.project_name - ) + if self.settings_file: + settings_s = settings_s + "SETTINGS_FILE='{0!s}'\n".format( + (self.settings_file) + ) + else: + settings_s = settings_s + "SETTINGS_FILE=None\n" - # since includes are for slim handler add the setting here by joining arbitrary list from zappa_settings file - # and tell the handler we are the slim_handler - # https://github.com/Miserlou/Zappa/issues/776 - settings_s += "SLIM_HANDLER=True\n" - - include = self.stage_config.get("include", []) - if len(include) >= 1: - settings_s += "INCLUDE=" + str(include) + "\n" - - # AWS Events function mapping - event_mapping = {} - events = self.stage_config.get("events", []) - for event in events: - arn = event.get("event_source", {}).get("arn") - function = event.get("function") - if arn and function: - event_mapping[arn] = function - settings_s = settings_s + "AWS_EVENT_MAPPING={0!s}\n".format(event_mapping) - - # Map Lext bot events - bot_events = self.stage_config.get("bot_events", []) - bot_events_mapping = {} - for bot_event in bot_events: - event_source = bot_event.get("event_source", {}) - intent = event_source.get("intent") - invocation_source = event_source.get("invocation_source") - function = bot_event.get("function") - if intent and invocation_source and function: - bot_events_mapping[ - str(intent) + ":" + str(invocation_source) - ] = function - - settings_s = settings_s + "AWS_BOT_EVENT_MAPPING={0!s}\n".format( - bot_events_mapping - ) - - # Map cognito triggers - cognito_trigger_mapping = {} - cognito_config = self.stage_config.get("cognito", {}) - triggers = cognito_config.get("triggers", []) - for trigger in triggers: - source = trigger.get("source") - function = trigger.get("function") - if source and function: - cognito_trigger_mapping[source] = function - settings_s = settings_s + "COGNITO_TRIGGER_MAPPING={0!s}\n".format( - cognito_trigger_mapping - ) - - # Authorizer config - authorizer_function = self.authorizer.get("function", None) - if authorizer_function: - settings_s += "AUTHORIZER_FUNCTION='{0!s}'\n".format( - authorizer_function - ) + if self.django_settings: + settings_s = settings_s + "DJANGO_SETTINGS='{0!s}'\n".format( + (self.django_settings) + ) + else: + settings_s = settings_s + "DJANGO_SETTINGS=None\n" - # Copy our Django app into root of our package. - # It doesn't work otherwise. - if self.django_settings: - base = __file__.rsplit(os.sep, 1)[0] - django_py = "".join(os.path.join(base, "ext", "django_zappa.py")) - lambda_zip.write(django_py, "django_zappa_app.py") + # If slim handler, path to project zip + if self.stage_config.get("slim_handler", False): + settings_s += "ARCHIVE_PATH='s3://{0!s}/{1!s}_{2!s}_current_project.tar.gz'\n".format( + self.s3_bucket_name, self.api_stage, self.project_name + ) - # async response - async_response_table = self.stage_config.get("async_response_table", "") - settings_s += "ASYNC_RESPONSE_TABLE='{0!s}'\n".format(async_response_table) + # since includes are for slim handler add the setting here by joining arbitrary list from zappa_settings file + # and tell the handler we are the slim_handler + # https://github.com/Miserlou/Zappa/issues/776 + settings_s += "SLIM_HANDLER=True\n" - # Lambda requires a specific chmod - temp_settings = tempfile.NamedTemporaryFile(delete=False) - os.chmod(temp_settings.name, 0o644) - temp_settings.write(bytes(settings_s, "utf-8")) - temp_settings.close() - lambda_zip.write(temp_settings.name, "zappa_settings.py") - os.unlink(temp_settings.name) + include = self.stage_config.get("include", []) + if len(include) >= 1: + settings_s += "INCLUDE=" + str(include) + "\n" + + # AWS Events function mapping + event_mapping = {} + events = self.stage_config.get("events", []) + for event in events: + arn = event.get("event_source", {}).get("arn") + function = event.get("function") + if arn and function: + event_mapping[arn] = function + settings_s = settings_s + "AWS_EVENT_MAPPING={0!s}\n".format(event_mapping) + + # Map Lext bot events + bot_events = self.stage_config.get("bot_events", []) + bot_events_mapping = {} + for bot_event in bot_events: + event_source = bot_event.get("event_source", {}) + intent = event_source.get("intent") + invocation_source = event_source.get("invocation_source") + function = bot_event.get("function") + if intent and invocation_source and function: + bot_events_mapping[ + str(intent) + ":" + str(invocation_source) + ] = function + + settings_s = settings_s + "AWS_BOT_EVENT_MAPPING={0!s}\n".format( + bot_events_mapping + ) + + # Map cognito triggers + cognito_trigger_mapping = {} + cognito_config = self.stage_config.get("cognito", {}) + triggers = cognito_config.get("triggers", []) + for trigger in triggers: + source = trigger.get("source") + function = trigger.get("function") + if source and function: + cognito_trigger_mapping[source] = function + settings_s = settings_s + "COGNITO_TRIGGER_MAPPING={0!s}\n".format( + cognito_trigger_mapping + ) + + # Authorizer config + authorizer_function = self.authorizer.get("function", None) + if authorizer_function: + settings_s += "AUTHORIZER_FUNCTION='{0!s}'\n".format(authorizer_function) + + # async response + async_response_table = self.stage_config.get("async_response_table", "") + settings_s += "ASYNC_RESPONSE_TABLE='{0!s}'\n".format(async_response_table) + return settings_s def remove_local_zip(self): """ @@ -2979,7 +3018,6 @@ def print_logs( ): """ Parse, filter and print logs to the console. - """ for log in logs: @@ -3146,7 +3184,6 @@ def colorize_log_entry(self, string): def execute_prebuild_script(self): """ Parse and execute the prebuild_script from the zappa_settings. - """ (pb_mod_path, pb_func) = self.prebuild_script.rsplit(".", 1) @@ -3197,7 +3234,6 @@ def collision_warning(self, item): """ Given a string, print a warning if this could collide with a Zappa core package module. - Use for app functions and events. """ diff --git a/zappa/core.py b/zappa/core.py index 3b4a76445..d4737727e 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -249,9 +249,7 @@ class Zappa: """ Zappa! - Makes it easy to run Python web applications on AWS Lambda/API Gateway. - """ ## @@ -561,9 +559,7 @@ def create_lambda_zip( ): """ Create a Lambda-ready zip file of the current virtualenvironment and working directory. - Returns path to that file. - """ # Validate archive_format if archive_format not in ["zip", "tarball"]: @@ -995,10 +991,8 @@ def get_manylinux_wheel_url(self, package_name, package_version): """ For a given package name, returns a link to the download URL, else returns None. - Related: https://github.com/Miserlou/Zappa/issues/398 Examples here: https://gist.github.com/perrygeo/9545f94eaddec18a65fd7b56880adbae - This function downloads metadata JSON of `package_name` from Pypi and examines if the package has a manylinux wheel. This function also caches the JSON file so that we don't have to poll Pypi @@ -1048,9 +1042,7 @@ def upload_to_s3(self, source_path, bucket_name, disable_progress=False): r""" Given a file, upload it to S3. Credentials should be stored in environment variables or ~/.aws/credentials (%USERPROFILE%\.aws\credentials on Windows). - Returns True on success, false on failure. - """ try: self.s3_client.head_bucket(Bucket=bucket_name) @@ -1134,11 +1126,8 @@ def copy_on_s3(self, src_file_name, dst_file_name, bucket_name): def remove_from_s3(self, file_name, bucket_name): """ Given a file name and a bucket, remove it from S3. - There's no reason to keep the file hosted on S3 once its been made into a Lambda function, so we can delete it from S3. - Returns True on success, False on failure. - """ try: self.s3_client.head_bucket(Bucket=bucket_name) @@ -1182,6 +1171,7 @@ def create_lambda_function( use_alb=False, layers=None, concurrency=None, + docker_image_uri=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. @@ -1201,9 +1191,7 @@ def create_lambda_function( kwargs = dict( FunctionName=function_name, - Runtime=runtime, Role=self.credentials_arn, - Handler=handler, Description=description, Timeout=timeout, MemorySize=memory_size, @@ -1215,11 +1203,23 @@ def create_lambda_function( TracingConfig={"Mode": "Active" if self.xray_tracing else "PassThrough"}, Layers=layers, ) - if local_zip: + if not docker_image_uri: + kwargs["Runtime"] = runtime + kwargs["Handler"] = handler + kwargs["PackageType"] = "Zip" + + if docker_image_uri: + kwargs["Code"] = {"ImageUri": docker_image_uri} + # default is ZIP. override to Image for container support + kwargs["PackageType"] = "Image" + # The create function operation times out when this is '' (the default) + # So just remove it from the kwargs if it is not specified + if aws_kms_key_arn == "": + kwargs.pop("KMSKeyArn") + elif local_zip: kwargs["Code"] = {"ZipFile": local_zip} else: kwargs["Code"] = {"S3Bucket": bucket, "S3Key": s3_key} - response = self.lambda_client.create_function(**kwargs) resource_arn = response["FunctionArn"] version = response["Version"] @@ -1255,6 +1255,7 @@ def update_lambda_function( local_zip=None, num_revisions=None, concurrency=None, + docker_image_uri=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. @@ -1263,7 +1264,9 @@ def update_lambda_function( print("Updating Lambda function code..") kwargs = dict(FunctionName=function_name, Publish=publish) - if local_zip: + if docker_image_uri: + kwargs["ImageUri"] = docker_image_uri + elif local_zip: kwargs["ZipFile"] = local_zip else: kwargs["S3Bucket"] = bucket @@ -1372,20 +1375,24 @@ def update_lambda_configuration( if key not in aws_environment_variables: aws_environment_variables[key] = value - response = self.lambda_client.update_function_configuration( - FunctionName=function_name, - Runtime=runtime, - Role=self.credentials_arn, - Handler=handler, - Description=description, - Timeout=timeout, - MemorySize=memory_size, - VpcConfig=vpc_config, - Environment={"Variables": aws_environment_variables}, - KMSKeyArn=aws_kms_key_arn, - TracingConfig={"Mode": "Active" if self.xray_tracing else "PassThrough"}, - Layers=layers, - ) + kwargs = { + "FunctionName": function_name, + "Role": self.credentials_arn, + "Description": description, + "Timeout": timeout, + "MemorySize": memory_size, + "VpcConfig": vpc_config, + "Environment": {"Variables": aws_environment_variables}, + "KMSKeyArn": aws_kms_key_arn, + "TracingConfig": {"Mode": "Active" if self.xray_tracing else "PassThrough"}, + } + + if lambda_aws_config["PackageType"] != "Image": + kwargs.update( + {"Handler": handler, "Runtime": runtime, "Layers": layers,} + ) + + response = self.lambda_client.update_function_configuration(**kwargs) resource_arn = response["FunctionArn"] @@ -1419,13 +1426,21 @@ def rollback_lambda_function_version( ): """ Rollback the lambda function code 'versions_back' number of revisions. - Returns the Function ARN. """ response = self.lambda_client.list_versions_by_function( FunctionName=function_name ) + # https://github.com/Miserlou/Zappa/pull/2192 + if ( + len(response.get("Versions", [])) > 1 + and response["Versions"][-1]["PackageType"] == "Image" + ): + raise NotImplementedError( + "Zappa's rollback functionality is not available for Docker based deployments" + ) + # Take into account $LATEST if len(response["Versions"]) < versions_back + 1: print("We do not have {} revisions. Aborting".format(str(versions_back))) @@ -1459,10 +1474,38 @@ def rollback_lambda_function_version( return response["FunctionArn"] + def is_lambda_function_ready(self, function_name): + """ + Checks if a lambda function is active and no updates are in progress. + """ + response = self.lambda_client.get_function(FunctionName=function_name) + return ( + response["Configuration"]["State"] == "Active" + and response["Configuration"]["LastUpdateStatus"] != "InProgress" + ) + + def wait_until_lambda_function_is_ready(self, function_name): + """ + Continuously check if a lambda function is active. + For functions deployed with a docker image instead of a + ZIP package, the function can take a few seconds longer + to be created or update, so we must wait before running any status + checks against the function. + """ + show_waiting_message = True + while True: + if self.is_lambda_function_ready(function_name): + break + + if show_waiting_message: + print("Waiting until lambda function is ready.") + show_waiting_message = False + + time.sleep(1) + def get_lambda_function(self, function_name): """ Returns the lambda function ARN, given a name - This requires the "lambda:GetFunction" role. """ response = self.lambda_client.get_function(FunctionName=function_name) @@ -1471,7 +1514,6 @@ def get_lambda_function(self, function_name): def get_lambda_function_versions(self, function_name): """ Simply returns the versions available for a Lambda function, given a function name. - """ try: response = self.lambda_client.list_versions_by_function( @@ -1484,9 +1526,7 @@ def get_lambda_function_versions(self, function_name): def delete_lambda_function(self, function_name): """ Given a function name, delete it from AWS Lambda. - Returns the response. - """ print("Deleting Lambda function..") @@ -1738,7 +1778,6 @@ def create_api_gateway_routes( ): """ Create the API Gateway for this Zappa deployment. - Returns the new RestAPI CF resource. """ @@ -1987,7 +2026,6 @@ def deploy_api_gateway( ): """ Deploy the API Gateway! - Return the deployed API URL. """ print("Deploying API Gateway..") @@ -2649,13 +2687,10 @@ def update_domain_name( """ This updates your certificate information for an existing domain, with similar arguments to boto's update_domain_name API Gateway api. - It returns the resulting new domain information including the new certificate's ARN if created during this process. - Previously, this method involved downtime that could take up to 40 minutes because the API Gateway api only allowed this by deleting, and then creating it. - Related issues: https://github.com/Miserlou/Zappa/issues/590 https://github.com/Miserlou/Zappa/issues/588 https://github.com/Miserlou/Zappa/pull/458 @@ -2754,9 +2789,7 @@ def get_all_zones(self): def get_domain_name(self, domain_name, route53=True): """ Scan our hosted zones for the record of a given name. - Returns the record entry, else None. - """ # Make sure api gateway domain is present try: @@ -2806,7 +2839,6 @@ def get_domain_name(self, domain_name, route53=True): def get_credentials_arn(self): """ Given our role name, get and set the credentials_arn. - """ role = self.iam.Role(self.role_name) self.credentials_arn = role.arn @@ -2815,7 +2847,6 @@ def get_credentials_arn(self): def create_iam_roles(self): """ Create and defines the IAM roles and policies necessary for Zappa. - If the IAM role already exists, it will be updated if necessary. """ attach_policy_obj = json.loads(self.attach_policy) @@ -2907,7 +2938,6 @@ def _clear_policy(self, lambda_name): def create_event_permission(self, lambda_name, principal, source_arn): """ Create permissions to link to an event. - Related: http://docs.aws.amazon.com/lambda/latest/dg/with-s3-example-configure-event-source.html """ logger.debug( @@ -2932,10 +2962,8 @@ def create_event_permission(self, lambda_name, principal, source_arn): def schedule_events(self, lambda_arn, lambda_name, events, default=True): """ Given a Lambda ARN, name and a list of events, schedule this as CloudWatch Events. - 'events' is a list of dictionaries, where the dict must contains the string of a 'function' and the string of the event 'expression', and an optional 'name' and 'description'. - Expressions can be in rate or cron format: http://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html """ @@ -3130,7 +3158,6 @@ def get_scheduled_event_name(event, function, lambda_name, index=0): def get_event_name(lambda_name, name): """ Returns an AWS-valid Lambda event name. - """ return "{prefix:.{width}}-{postfix}".format( prefix=lambda_name, width=max(0, 63 - len(name)), postfix=name @@ -3151,10 +3178,8 @@ def get_hashed_rule_name(event, function, lambda_name): def delete_rule(self, rule_name): """ Delete a CWE rule. - This deletes them, but they will still show up in the AWS console. Annoying. - """ logger.debug("Deleting existing rule {}".format(rule_name)) @@ -3210,7 +3235,6 @@ def unschedule_events( excluded_source_services = excluded_source_services or [] """ Given a list of events, unschedule these CloudWatch Events. - 'events' is a list of dictionaries, where the dict must contains the string of a 'function' and the string of the event 'expression', and an optional 'name' and 'description'. """ @@ -3411,7 +3435,6 @@ def remove_api_gateway_logs(self, project_name): def get_hosted_zone_id_for_domain(self, domain): """ Get the Hosted Zone ID for a given domain. - """ all_zones = self.get_all_zones() return self.get_best_match_zone(all_zones, domain) @@ -3473,7 +3496,6 @@ def get_dns_challenge_change_batch(action, domain, txt_challenge): """ Given action, domain and challenge, return a change batch to use with route53 call. - :param action: DELETE | UPSERT :param domain: domain name :param txt_challenge: challenge @@ -3508,9 +3530,7 @@ def shell(self): def load_credentials(self, boto_session=None, profile_name=None): """ Load AWS credentials. - An optional boto_session can be provided, but that's usually for testing. - An optional profile_name can be provided for config files that have multiple sets of credentials. """