PYTHON-3461 Test FaaS (AWS Lambda) Behavior Per Driver (#1310)

This commit is contained in:
Steven Silvester 2023-08-11 12:58:14 -05:00 committed by GitHub
parent 0d44783edd
commit 28b11219ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 387 additions and 0 deletions

View File

@ -1303,6 +1303,33 @@ task_groups:
tasks:
- testazurekms-task
- name: test_aws_lambda_task_group
setup_group:
- func: fetch source
- func: prepare resources
- command: subprocess.exec
params:
working_dir: src
binary: bash
add_expansions_to_env: true
args:
- ${DRIVERS_TOOLS}/.evergreen/atlas/setup-atlas-cluster.sh
- command: expansions.update
params:
file: src/atlas-expansion.yml
teardown_group:
- command: subprocess.exec
params:
working_dir: src
binary: bash
add_expansions_to_env: true
args:
- ${DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800
tasks:
- test-aws-lambda-deployed
- name: test_atlas_task_group_search_indexes
setup_group:
- func: fetch source
@ -1785,6 +1812,23 @@ tasks:
vars:
TEST_DATA_LAKE: "true"
- name: "test-aws-lambda-deployed"
commands:
- func: "install dependencies"
- command: ec2.assume_role
params:
role_arn: ${LAMBDA_AWS_ROLE_ARN}
duration_seconds: 3600
- command: subprocess.exec
params:
working_dir: src
binary: bash
add_expansions_to_env: true
args:
- .evergreen/run-deployed-lambda-aws-tests.sh
env:
TEST_LAMBDA_DIRECTORY: ${PROJECT_DIRECTORY}/test/lambda
- name: test-ocsp-rsa-valid-cert-server-staples
tags: ["ocsp", "ocsp-rsa", "ocsp-staple"]
commands:
@ -3358,6 +3402,12 @@ buildvariants:
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README
- testazurekms-fail-task
- name: rhel8-test-lambda
display_name: AWS Lambda handler tests
run_on: rhel87-small
tasks:
- name: test_aws_lambda_task_group
- name: Release
display_name: Release
batchtime: 20160 # 14 days

View File

@ -0,0 +1,10 @@
#!/bin/bash
set -o errexit # Exit the script with error if any of the commands fail
export PATH="/opt/python/3.9/bin:${PATH}"
python --version
pushd ./test/lambda
. build.sh
popd
. ${DRIVERS_TOOLS}/.evergreen/aws_lambda/run-deployed-lambda-aws-tests.sh

7
.gitignore vendored
View File

@ -17,3 +17,10 @@ mongocryptd.pid
.idea/
.nova/
venv/
# Lambda temp files
test/lambda/.aws-sam
test/lambda/env.json
test/lambda/mongodb/pymongo/*
test/lambda/mongodb/gridfs/*
test/lambda/mongodb/bson/*

View File

@ -7,6 +7,7 @@ repos:
- id: check-case-conflict
- id: check-toml
- id: check-yaml
exclude: template.yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: WHEEL

17
test/lambda/README.md Normal file
View File

@ -0,0 +1,17 @@
AWS Lambda Testing
------------------
Running locally
===============
Prerequisites:
- AWS SAM CLI
- Docker daemon running
Usage
=====
- Start a local mongodb instance on port 27017
- Run ``build.sh``
- Run ``test.sh``

28
test/lambda/build.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
set -o errexit # Exit the script with error if any of the commands fail
set -o xtrace
rm -rf mongodb/pymongo
rm -rf mongodb/gridfs
rm -rf mongodb/bson
pushd ../..
rm -f pymongo/*.so
rm -f bson/*.so
image="quay.io/pypa/manylinux2014_x86_64:latest"
DOCKER=$(command -v docker) || true
if [ -z "$DOCKER" ]; then
PODMAN=$(command -v podman) || true
if [ -z "$PODMAN" ]; then
echo "docker or podman are required!"
exit 1
fi
DOCKER=podman
fi
$DOCKER run --rm -v "`pwd`:/src" $image /src/test/lambda/build_internal.sh
cp -r pymongo ./test/lambda/mongodb/pymongo
cp -r bson ./test/lambda/mongodb/bson
cp -r gridfs ./test/lambda/mongodb/gridfs
popd

5
test/lambda/build_internal.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash -ex
cd /src
PYTHON=/opt/python/cp39-cp39/bin/python
$PYTHON -m pip install -v -e .

View File

@ -0,0 +1,62 @@
{
"body": "{\"message\": \"hello world\"}",
"resource": "/hello",
"path": "/hello",
"httpMethod": "GET",
"isBase64Encoded": false,
"queryStringParameters": {
"foo": "bar"
},
"pathParameters": {
"proxy": "/path/to/resource"
},
"stageVariables": {
"baz": "qux"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, sdch",
"Accept-Language": "en-US,en;q=0.8",
"Cache-Control": "max-age=0",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Custom User Agent String",
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"requestContext": {
"accountId": "123456789012",
"resourceId": "123456",
"stage": "prod",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"requestTime": "09/Apr/2015:12:34:56 +0000",
"requestTimeEpoch": 1428582896000,
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"accessKey": null,
"sourceIp": "127.0.0.1",
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "Custom User Agent String",
"user": null
},
"path": "/prod/hello",
"resourcePath": "/hello",
"httpMethod": "POST",
"apiId": "1234567890",
"protocol": "HTTP/1.1"
}
}

View File

@ -0,0 +1,4 @@
build-MongoDBFunction:
cp -r . $(ARTIFACTS_DIR)
python -m pip install -t $(ARTIFACTS_DIR) dnspython

View File

149
test/lambda/mongodb/app.py Normal file
View File

@ -0,0 +1,149 @@
"""
Lambda function for Python Driver testing
Creates the client that is cached for all requests, subscribes to
relevant events, and forces the connection pool to get populated.
"""
import json
import os
from bson import has_c as has_bson_c
from pymongo import MongoClient
from pymongo import has_c as has_pymongo_c
from pymongo.monitoring import (
CommandListener,
ConnectionPoolListener,
ServerHeartbeatListener,
)
open_connections = 0
heartbeat_count = 0
total_heartbeat_duration = 0
total_commands = 0
total_command_duration = 0
# Ensure we are using C extensions
assert has_bson_c()
assert has_pymongo_c()
class CommandHandler(CommandListener):
def started(self, event):
print("command started", event)
def succeeded(self, event):
global total_commands, total_command_duration
total_commands += 1
total_command_duration += event.duration_micros / 1e6
print("command succeeded", event)
def failed(self, event):
global total_commands, total_command_duration
total_commands += 1
total_command_duration += event.duration_micros / 1e6
print("command failed", event)
class ServerHeartbeatHandler(ServerHeartbeatListener):
def started(self, event):
print("server heartbeat started", event)
def succeeded(self, event):
global heartbeat_count, total_heartbeat_duration
heartbeat_count += 1
total_heartbeat_duration += event.duration
print("server heartbeat succeeded", event)
def failed(self, event):
global heartbeat_count, total_heartbeat_duration
heartbeat_count += 1
total_heartbeat_duration += event.duration
print("server heartbeat failed", event)
class ConnectionHandler(ConnectionPoolListener):
def connection_created(self, event):
global open_connections
open_connections += 1
print("connection created")
def connection_ready(self, event):
pass
def connection_closed(self, event):
global open_connections
open_connections -= 1
print("connection closed")
def connection_check_out_started(self, event):
pass
def connection_check_out_failed(self, event):
pass
def connection_checked_out(self, event):
pass
def connection_checked_in(self, event):
pass
def pool_created(self, event):
pass
def pool_ready(self, event):
pass
def pool_cleared(self, event):
pass
def pool_closed(self, event):
pass
listeners = [CommandHandler(), ServerHeartbeatHandler(), ConnectionHandler()]
print("Creating client")
client = MongoClient(os.environ["MONGODB_URI"], event_listeners=listeners)
# Populate the connection pool.
print("Connecting")
client.lambdaTest.list_collections()
print("Connected")
# Create the response to send back.
def create_response():
return dict(
averageCommandDuration=total_command_duration / total_commands,
averageHeartbeatDuration=total_heartbeat_duration / heartbeat_count,
openConnections=open_connections,
heartbeatCount=heartbeat_count,
)
# Reset the numbers.
def reset():
global open_connections, heartbeat_count, total_heartbeat_duration, total_commands, total_command_duration
open_connections = 0
heartbeat_count = 0
total_heartbeat_duration = 0
total_commands = 0
total_command_duration = 0
def lambda_handler(event, context):
"""
The handler function itself performs an insert/delete and returns the
id of the document in play.
"""
print("initializing")
db = client.lambdaTest
collection = db.test
result = collection.insert_one({"n": 1})
collection.delete_one({"_id": result.inserted_id})
# Create the response and then reset the numbers.
response = json.dumps(create_response())
reset()
print("finished!")
return dict(statusCode=200, body=response)

5
test/lambda/run.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -o errexit # Exit the script with error if any of the commands fail
sam build
sam local invoke --docker-network host --parameter-overrides "MongoDbUri=mongodb://host.docker.internal:27017"

49
test/lambda/template.yaml Normal file
View File

@ -0,0 +1,49 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
Python driver lambda function test
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 30
MemorySize: 128
Parameters:
MongoDbUri:
Type: String
Description: The MongoDB connection string.
Resources:
MongoDBFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: mongodb/
Environment:
Variables:
MONGODB_URI: !Ref MongoDbUri
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
Events:
MongoDB:
Type: Api
Properties:
Path: /mongodb
Method: get
# Use a custom build method to make sure *.so files are copied.
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-custom-runtimes.html
Metadata:
BuildMethod: makefile
Outputs:
MongoDBApi:
Description: "API Gateway endpoint URL for Prod stage for Python driver lambda function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
MongoDBFunction:
Description: "Python driver lambda Function ARN"
Value: !GetAtt MongoDBFunction.Arn
MongoDBFunctionIamRole:
Description: "Implicit IAM Role created for Python driver lambda function"
Value: !GetAtt MongoDBFunctionRole.Arn