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 5cc6b882a..35e02ac28 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -366,7 +366,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 = {"Environment": {"Variables": {"REMOTE_ONLY": "AAA", "CHANGED_REMOTE" : "BBB"}}} + mock_client.get_function_configuration.return_value = { + "PackageType": "Zip", + "Environment": {"Variables": {"REMOTE_ONLY": "AAA", "CHANGED_REMOTE" : "BBB"}} + } z.update_lambda_configuration("test", "test", "test", aws_environment_variables={"CHANGED_REMOTE" : "ZZ", "LOCAL_ONLY" : "YY"}) end_result_should_be = {"REMOTE_ONLY": "AAA", "CHANGED_REMOTE" : "ZZ", "LOCAL_ONLY" : "YY"} self.assertEqual(mock_client.update_function_configuration.call_args[1]["Environment"], { "Variables": end_result_should_be}) @@ -374,7 +377,9 @@ 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 = { - "Environment": {"Variables": {"REMOTE_ONLY_1": "AAA", "REMOTE_ONLY_2": "BBB"}}} + "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"} self.assertEqual(mock_client.update_function_configuration.call_args[1]["Environment"], @@ -386,11 +391,11 @@ 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"]) self.assertEqual(mock_client.update_function_configuration.call_args[1]["Layers"], ["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"], []) @@ -401,7 +406,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", "test", aws_environment_variables={"LOCAL_ONLY" : "LZ", "SHOW_AND_TELL" : "SHA"}) end_result_should_be = {"LOCAL_ONLY" : "LZ", "SHOW_AND_TELL" : "SHA"} self.assertEqual(mock_client.update_function_configuration.call_args[1]["Environment"], { "Variables": end_result_should_be}) @@ -932,6 +937,23 @@ 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 47e477173..68b098d76 100644 --- a/tests/tests_placebo.py +++ b/tests/tests_placebo.py @@ -132,6 +132,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) @@ -143,6 +165,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 9b3361c4e..987925cc7 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -73,7 +73,6 @@ class ZappaCLI: """ ZappaCLI object is responsible for loading the settings, handling the input arguments and executing the calls to the core library. - """ # CLI @@ -180,9 +179,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' @@ -254,6 +251,9 @@ def handle(self, argv=None): deploy_parser.add_argument( '-z', '--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 @@ -429,6 +429,9 @@ def positive_int(s): update_parser.add_argument( '-n', '--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 @@ -437,6 +440,22 @@ def positive_int(s): 'shell', parents=[env_parser], 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) @@ -546,7 +565,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 @@ -556,7 +575,7 @@ 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 @@ -621,11 +640,22 @@ def dispatch_command(self, command, stage): ) 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 @@ -682,27 +712,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: @@ -719,6 +735,21 @@ def deploy(self, source_zip=None): bold=True) + '\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') @@ -768,18 +799,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 @@ -845,27 +876,28 @@ def deploy(self, source_zip=None): 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_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() @@ -943,7 +975,11 @@ 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, @@ -961,7 +997,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. @@ -980,7 +1016,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() @@ -1056,6 +1092,7 @@ 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: @@ -1077,7 +1114,6 @@ def rollback(self, revision): def tail(self, since, filter_pattern, limit=10000, keep_open=True, colorize=True, http=False, non_http=False, force_colorize=False): """ Tail this function's logs. - if keep_open, do so repeatedly, printing any new logs """ @@ -1163,7 +1199,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', []) @@ -1235,7 +1270,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 []). @@ -1331,7 +1365,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(). """ @@ -1422,13 +1455,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') else: @@ -1530,7 +1565,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 @@ -1543,7 +1577,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) """ @@ -1559,10 +1592,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. @@ -1768,7 +1799,7 @@ def init(self, settings_file="zappa_settings.json"): } zappa_settings.update(g_env) - import json as json # hjson is fine for loading, not fine for writing. + import json as json # hjson is fine for loading, not fine for writing. zappa_settings_json = json.dumps(zappa_settings, sort_keys=True, indent=4) click.echo("\nOkay, here's your " + click.style("zappa_settings.json", bold=True) + ":\n") @@ -1945,7 +1976,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 """ @@ -2005,9 +2035,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. """ @@ -2205,7 +2233,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) @@ -2278,164 +2305,168 @@ def create_package(self, output=None): with zipfile.ZipFile(handler_zip, 'a') as lambda_zip: - settings_s = "# Generated by Zappa\n" + settings_s = self.get_zappa_settings_string() - 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) + # 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 self.exception_handler: - settings_s += "EXCEPTION_HANDLER='{0!s}'\n".format(self.exception_handler) - else: - settings_s += "EXCEPTION_HANDLER=None\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) - if self.debug: - settings_s = settings_s + "DEBUG=True\n" - else: - settings_s = settings_s + "DEBUG=False\n" + def get_zappa_settings_string(self): + settings_s = "# Generated by Zappa\n" - settings_s = settings_s + "LOG_LEVEL='{0!s}'\n".format((self.log_level)) + 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 self.exception_handler: + settings_s += "EXCEPTION_HANDLER='{0!s}'\n".format(self.exception_handler) + else: + settings_s += "EXCEPTION_HANDLER=None\n" - if self.binary_support: - settings_s = settings_s + "BINARY_SUPPORT=True\n" - else: - settings_s = settings_s + "BINARY_SUPPORT=False\n" + if self.debug: + settings_s = settings_s + "DEBUG=True\n" + else: + settings_s = settings_s + "DEBUG=False\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 - ) + settings_s = settings_s + "LOG_LEVEL='{0!s}'\n".format((self.log_level)) - # 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.binary_support: + settings_s = settings_s + "BINARY_SUPPORT=True\n" + else: + settings_s = settings_s + "BINARY_SUPPORT=False\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" + 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 + ) - # 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 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.base_path: + settings_s = settings_s + "BASE_PATH='{0!s}'\n".format((self.base_path)) + else: + settings_s = settings_s + "BASE_PATH=None\n" - # Local envs - env_dict = {} - if self.aws_region: - env_dict['AWS_REGION'] = self.aws_region - env_dict.update(dict(self.environment_variables)) + # 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 + ) - # 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.") + # 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 + "ENVIRONMENT_VARIABLES={0}\n".format( - env_dict - ) + # 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.") - # 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)) + settings_s = settings_s + "ENVIRONMENT_VARIABLES={0}\n".format( + env_dict + ) - 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" + # 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.django_settings: - settings_s = settings_s + "DJANGO_SETTINGS='{0!s}'\n".format((self.django_settings)) - else: - settings_s = settings_s + "DJANGO_SETTINGS=None\n" + 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" - # 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) - - # 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) + 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" - # Authorizer config - authorizer_function = self.authorizer.get('function', None) - if authorizer_function: - settings_s += "AUTHORIZER_FUNCTION='{0!s}'\n".format(authorizer_function) + # 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) - # 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') + # 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" - # async response - async_response_table = self.stage_config.get('async_response_table', '') - settings_s += "ASYNC_RESPONSE_TABLE='{0!s}'\n".format(async_response_table) + include = self.stage_config.get('include', []) + if len(include) >= 1: + settings_s += "INCLUDE=" + str(include) + '\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) + # 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): """ @@ -2478,7 +2509,6 @@ def on_exit(self): def print_logs(self, logs, colorize=True, http=False, non_http=False, force_colorize=None): """ Parse, filter and print logs to the console. - """ for log in logs: @@ -2602,7 +2632,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) @@ -2641,7 +2670,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 bbbf0bb10..d96acc2e2 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -213,9 +213,7 @@ class Zappa: """ Zappa! - Makes it easy to run Python web applications on AWS Lambda/API Gateway. - """ ## @@ -479,9 +477,7 @@ def create_lambda_zip( self, ): """ 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']: @@ -839,10 +835,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 @@ -890,9 +884,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) @@ -976,11 +968,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) @@ -1020,6 +1009,7 @@ def create_lambda_function( self, 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. @@ -1039,9 +1029,7 @@ def create_lambda_function( self, kwargs = dict( FunctionName=function_name, - Runtime=runtime, Role=self.credentials_arn, - Handler=handler, Description=description, Timeout=timeout, MemorySize=memory_size, @@ -1055,7 +1043,22 @@ def create_lambda_function( self, }, 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 } @@ -1064,7 +1067,6 @@ def create_lambda_function( self, 'S3Bucket': bucket, 'S3Key': s3_key } - response = self.lambda_client.create_function(**kwargs) resource_arn = response['FunctionArn'] version = response['Version'] @@ -1092,7 +1094,7 @@ def create_lambda_function( self, return resource_arn - def update_lambda_function(self, bucket, function_name, s3_key=None, publish=True, local_zip=None, num_revisions=None, concurrency=None): + def update_lambda_function(self, bucket, function_name, s3_key=None, publish=True, 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. Optionally, delete previous versions if they exceed the optional limit. @@ -1103,7 +1105,9 @@ def update_lambda_function(self, bucket, function_name, s3_key=None, publish=Tru 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 @@ -1198,6 +1202,7 @@ def update_lambda_configuration( self, # Check if there are any remote aws lambda env vars so they don't get trashed. # https://github.com/Miserlou/Zappa/issues/987, Related: https://github.com/Miserlou/Zappa/issues/765 lambda_aws_config = self.lambda_client.get_function_configuration(FunctionName=function_name) + is_docker_deployment = lambda_aws_config['PackageType'] == 'Image' if "Environment" in lambda_aws_config: lambda_aws_environment_variables = lambda_aws_config["Environment"].get("Variables", {}) # Append keys that are remote but not in settings file @@ -1205,22 +1210,26 @@ def update_lambda_configuration( self, 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={ + 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' }, - Layers=layers - ) + } + + if not is_docker_deployment: + kwargs['Handler'] = handler + kwargs['Runtime'] = runtime + kwargs['Layers'] = layers + + response = self.lambda_client.update_function_configuration(**kwargs) resource_arn = response['FunctionArn'] @@ -1251,11 +1260,16 @@ def invoke_lambda_function( self, def rollback_lambda_function_version(self, function_name, versions_back=1, publish=True): """ 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))) @@ -1275,10 +1289,39 @@ def rollback_lambda_function_version(self, function_name, versions_back=1, publi 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( @@ -1288,7 +1331,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( @@ -1301,9 +1343,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..") @@ -1537,7 +1577,6 @@ def create_api_gateway_routes( self, ): """ Create the API Gateway for this Zappa deployment. - Returns the new RestAPI CF resource. """ @@ -1767,7 +1806,6 @@ def deploy_api_gateway( self, ): """ Deploy the API Gateway! - Return the deployed API URL. """ print("Deploying API Gateway..") @@ -2400,13 +2438,10 @@ def update_domain_name(self, """ 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 @@ -2482,9 +2517,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: @@ -2529,7 +2562,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 @@ -2538,7 +2570,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) @@ -2622,7 +2653,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('Adding new permission to invoke Lambda function: {}'.format(lambda_name)) @@ -2643,10 +2673,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 """ @@ -2800,7 +2828,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)[:64] @@ -2817,10 +2844,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)) @@ -2869,7 +2894,6 @@ def unschedule_events(self, events, lambda_arn=None, lambda_name=None, excluded_ 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'. """ @@ -3089,7 +3113,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) @@ -3137,7 +3160,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 @@ -3171,9 +3193,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. """