diff --git a/.gitignore b/.gitignore index 94f3d847..bb7ec9b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target /.cargo lambda-runtime/libtest.rmeta +lambda-integration-tests/target Cargo.lock # Built AWS Lambda zipfile @@ -8,3 +9,6 @@ lambda.zip # output.json from example docs output.json + +.aws-sam +build \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 01bc46b3..4ef3789d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "lambda-http", + "lambda-integration-tests", "lambda-runtime-api-client", "lambda-runtime", "lambda-extension" diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..62cb2171 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +INTEG_STACK_NAME ?= rust-lambda-integration-tests +INTEG_FUNCTIONS_BUILD := runtime-fn runtime-trait http-fn +INTEG_FUNCTIONS_INVOKE := RuntimeFn RuntimeFnAl2 RuntimeTrait RuntimeTraitAl2 Python PythonAl2 +INTEG_API_INVOKE := RestApiUrl HttpApiUrl +INTEG_EXTENSIONS := extension-fn extension-trait +# Using musl to run extensions on both AL1 and AL2 +INTEG_ARCH := x86_64-unknown-linux-musl + +integration-tests: +# Build Integration functions + cross build --release --target $(INTEG_ARCH) -p lambda_integration_tests + rm -rf ./build + mkdir -p ./build + ${MAKE} ${MAKEOPTS} $(foreach function,${INTEG_FUNCTIONS_BUILD}, build-integration-function-${function}) + ${MAKE} ${MAKEOPTS} $(foreach extension,${INTEG_EXTENSIONS}, build-integration-extension-${extension}) +# Deploy to AWS + sam deploy \ + --template lambda-integration-tests/template.yaml \ + --stack-name ${INTEG_STACK_NAME} \ + --capabilities CAPABILITY_IAM \ + --resolve-s3 \ + --no-fail-on-empty-changeset +# Invoke functions + ${MAKE} ${MAKEOPTS} $(foreach function,${INTEG_FUNCTIONS_INVOKE}, invoke-integration-function-${function}) + ${MAKE} ${MAKEOPTS} $(foreach api,${INTEG_API_INVOKE}, invoke-integration-api-${api}) + +build-integration-function-%: + mkdir -p ./build/$* + cp -v ./target/$(INTEG_ARCH)/release/$* ./build/$*/bootstrap + +build-integration-extension-%: + mkdir -p ./build/$*/extensions + cp -v ./target/$(INTEG_ARCH)/release/$* ./build/$*/extensions/$* + +invoke-integration-function-%: + aws lambda invoke --function-name $$(aws cloudformation describe-stacks --stack-name $(INTEG_STACK_NAME) \ + --query 'Stacks[0].Outputs[?OutputKey==`$*`].OutputValue' \ + --output text) --payload '{"command": "hello"}' --cli-binary-format raw-in-base64-out /dev/stdout + +invoke-integration-api-%: + $(eval API_URL := $(shell aws cloudformation describe-stacks --stack-name $(INTEG_STACK_NAME) \ + --query 'Stacks[0].Outputs[?OutputKey==`$*`].OutputValue' \ + --output text)) + curl $(API_URL)/get + curl $(API_URL)/al2/get + curl -X POST -d '{"command": "hello"}' $(API_URL)/post + curl -X POST -d '{"command": "hello"}' $(API_URL)/al2/post + \ No newline at end of file diff --git a/lambda-extension/src/lib.rs b/lambda-extension/src/lib.rs index 41f56890..f1d34a18 100644 --- a/lambda-extension/src/lib.rs +++ b/lambda-extension/src/lib.rs @@ -1,4 +1,5 @@ #![deny(clippy::all, clippy::cargo)] +#![allow(clippy::multiple_crate_versions)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] //! This module includes utilities to create Lambda Runtime Extensions. @@ -8,8 +9,7 @@ use hyper::client::{connect::Connection, HttpConnector}; use lambda_runtime_api_client::Client; use serde::Deserialize; -use std::future::Future; -use std::path::PathBuf; +use std::{future::Future, path::PathBuf}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_stream::StreamExt; use tower_service::Service; diff --git a/lambda-integration-tests/Cargo.toml b/lambda-integration-tests/Cargo.toml new file mode 100644 index 00000000..b250e911 --- /dev/null +++ b/lambda-integration-tests/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "lambda_integration_tests" +version = "0.4.1" +edition = "2018" +description = "AWS Lambda Runtime integration tests" +license = "Apache-2.0" +repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +categories = ["web-programming::http-server"] +keywords = ["AWS", "Lambda", "API"] +readme = "../README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lambda_http = { path = "../lambda-http", version = "0.4.1" } +lambda_runtime = { path = "../lambda-runtime", version = "0.4.1" } +lambda_extension = { path = "../lambda-extension", version = "0.1.0" } +log = "0.4" +serde = { version = "1", features = ["derive"] } +simple_logger = { version = "1.15", default-features = false } +tokio = { version = "1", features = ["full"] } \ No newline at end of file diff --git a/lambda-integration-tests/python/main.py b/lambda-integration-tests/python/main.py new file mode 100644 index 00000000..e7e2114b --- /dev/null +++ b/lambda-integration-tests/python/main.py @@ -0,0 +1,4 @@ +def handler(event, context): + return { + "message": event["command"].upper() + } \ No newline at end of file diff --git a/lambda-integration-tests/src/bin/extension-fn.rs b/lambda-integration-tests/src/bin/extension-fn.rs new file mode 100644 index 00000000..ad641d6c --- /dev/null +++ b/lambda-integration-tests/src/bin/extension-fn.rs @@ -0,0 +1,23 @@ +use lambda_extension::{extension_fn, Error, NextEvent}; +use log::{info, LevelFilter}; +use simple_logger::SimpleLogger; + +async fn my_extension(event: NextEvent) -> Result<(), Error> { + match event { + NextEvent::Shutdown(e) => { + info!("[extension-fn] Shutdown event received: {:?}", e); + } + NextEvent::Invoke(e) => { + info!("[extension-fn] Request event received: {:?}", e); + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); + + lambda_extension::run(extension_fn(my_extension)).await +} diff --git a/lambda-integration-tests/src/bin/extension-trait.rs b/lambda-integration-tests/src/bin/extension-trait.rs new file mode 100644 index 00000000..de9031e6 --- /dev/null +++ b/lambda-integration-tests/src/bin/extension-trait.rs @@ -0,0 +1,37 @@ +use lambda_extension::{Error, Extension, NextEvent}; +use log::{info, LevelFilter}; +use simple_logger::SimpleLogger; +use std::{ + future::{ready, Future}, + pin::Pin, +}; + +#[derive(Default)] +struct MyExtension { + invoke_count: usize, +} + +impl Extension for MyExtension { + type Fut = Pin>>>; + + fn call(&mut self, event: NextEvent) -> Self::Fut { + match event { + NextEvent::Shutdown(e) => { + info!("[extension] Shutdown event received: {:?}", e); + } + NextEvent::Invoke(e) => { + self.invoke_count += 1; + info!("[extension] Request event {} received: {:?}", self.invoke_count, e); + } + } + + Box::pin(ready(Ok(()))) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); + + lambda_extension::run(MyExtension::default()).await +} diff --git a/lambda-integration-tests/src/bin/http-fn.rs b/lambda-integration-tests/src/bin/http-fn.rs new file mode 100644 index 00000000..26be7535 --- /dev/null +++ b/lambda-integration-tests/src/bin/http-fn.rs @@ -0,0 +1,19 @@ +use lambda_http::{ + lambda_runtime::{self, Context, Error}, + IntoResponse, Request, Response, +}; +use log::{info, LevelFilter}; +use simple_logger::SimpleLogger; + +async fn handler(event: Request, _context: Context) -> Result { + info!("[http-fn] Received event {} {}", event.method(), event.uri().path()); + + Ok(Response::builder().status(200).body("Hello, world!").unwrap()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); + + lambda_runtime::run(lambda_http::handler(handler)).await +} diff --git a/lambda-integration-tests/src/bin/runtime-fn.rs b/lambda-integration-tests/src/bin/runtime-fn.rs new file mode 100644 index 00000000..37057aef --- /dev/null +++ b/lambda-integration-tests/src/bin/runtime-fn.rs @@ -0,0 +1,29 @@ +use lambda_runtime::{handler_fn, Context, Error}; +use log::{info, LevelFilter}; +use serde::{Deserialize, Serialize}; +use simple_logger::SimpleLogger; + +#[derive(Deserialize, Debug)] +struct Request { + command: String, +} + +#[derive(Serialize, Debug)] +struct Response { + message: String, +} + +async fn handler(event: Request, _context: Context) -> Result { + info!("[handler-fn] Received event: {:?}", event); + + Ok(Response { + message: event.command.to_uppercase(), + }) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); + + lambda_runtime::run(handler_fn(handler)).await +} diff --git a/lambda-integration-tests/src/bin/runtime-trait.rs b/lambda-integration-tests/src/bin/runtime-trait.rs new file mode 100644 index 00000000..e1de6bb1 --- /dev/null +++ b/lambda-integration-tests/src/bin/runtime-trait.rs @@ -0,0 +1,43 @@ +use lambda_runtime::{Context, Error, Handler}; +use log::{info, LevelFilter}; +use serde::{Deserialize, Serialize}; +use simple_logger::SimpleLogger; +use std::{ + future::{ready, Future}, + pin::Pin, +}; + +#[derive(Deserialize, Debug)] +struct Request { + command: String, +} + +#[derive(Serialize, Debug)] +struct Response { + message: String, +} + +#[derive(Default)] +struct MyHandler { + invoke_count: usize, +} + +impl Handler for MyHandler { + type Error = Error; + type Fut = Pin>>>; + + fn call(&mut self, event: Request, _context: Context) -> Self::Fut { + self.invoke_count += 1; + info!("[handler] Received event {}: {:?}", self.invoke_count, event); + Box::pin(ready(Ok(Response { + message: event.command.to_uppercase(), + }))) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); + + lambda_runtime::run(MyHandler::default()).await +} diff --git a/lambda-integration-tests/template.yaml b/lambda-integration-tests/template.yaml new file mode 100644 index 00000000..7f408d2c --- /dev/null +++ b/lambda-integration-tests/template.yaml @@ -0,0 +1,162 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Globals: + Function: + MemorySize: 128 + Handler: bootstrap + Timeout: 5 + +Resources: + # Rust function using runtime_fn running on AL2 + RuntimeFnAl2: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../build/runtime-fn/ + Runtime: provided.al2 + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + # Rust function using runtime_fn running on AL1 + RuntimeFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../build/runtime-fn/ + Runtime: provided + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + # Rust function using a Handler implementation running on AL2 + RuntimeTraitAl2: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../build/runtime-trait/ + Runtime: provided.al2 + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + # Rust function using a Handler implementation running on AL1 + RuntimeTrait: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../build/runtime-trait/ + Runtime: provided + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + # Rust function using lambda_http::runtime running on AL2 + HttpFnAl2: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../build/http-fn/ + Runtime: provided.al2 + Events: + ApiGet: + Type: Api + Properties: + Method: GET + Path: /al2/get + ApiPost: + Type: Api + Properties: + Method: POST + Path: /al2/post + ApiV2Get: + Type: HttpApi + Properties: + Method: GET + Path: /al2/get + ApiV2Post: + Type: HttpApi + Properties: + Method: POST + Path: /al2/post + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + # Rust function using lambda_http::runtime running on AL1 + HttpFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../build/http-fn/ + Runtime: provided + Events: + ApiGet: + Type: Api + Properties: + Method: GET + Path: /get + ApiPost: + Type: Api + Properties: + Method: POST + Path: /post + ApiV2Get: + Type: HttpApi + Properties: + Method: GET + Path: /get + ApiV2Post: + Type: HttpApi + Properties: + Method: POST + Path: /post + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + # Python function running on AL2 + PythonAl2: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./python/ + Handler: main.handler + Runtime: python3.9 + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + # Python function running on AL1 + Python: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./python/ + Handler: main.handler + Runtime: python3.7 + Layers: + - !Ref ExtensionFn + - !Ref ExtensionTrait + + ExtensionFn: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ../build/extension-fn/ + + ExtensionTrait: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ../build/extension-trait/ + +Outputs: + RuntimeFnAl2: + Value: !GetAtt RuntimeFnAl2.Arn + RuntimeFn: + Value: !GetAtt RuntimeFn.Arn + RuntimeTraitAl2: + Value: !GetAtt RuntimeTraitAl2.Arn + RuntimeTrait: + Value: !GetAtt RuntimeTrait.Arn + PythonAl2: + Value: !GetAtt PythonAl2.Arn + Python: + Value: !GetAtt Python.Arn + + RestApiUrl: + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" + HttpApiUrl: + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" \ No newline at end of file diff --git a/lambda-runtime/src/lib.rs b/lambda-runtime/src/lib.rs index 85403ea3..ad6fecf1 100644 --- a/lambda-runtime/src/lib.rs +++ b/lambda-runtime/src/lib.rs @@ -1,4 +1,5 @@ #![deny(clippy::all, clippy::cargo)] +#![allow(clippy::multiple_crate_versions)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] //! The mechanism available for defining a Lambda function is as follows: