diff --git a/.dockerignore b/.dockerignore index 39dc26852..05147945c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,3 +23,4 @@ pg_data/ config.yml tests/output/ tmp/ +.aws-sam/ diff --git a/.github/files/lambda-function/Makefile b/.github/files/lambda-function/Makefile new file mode 100644 index 000000000..1dfa25496 --- /dev/null +++ b/.github/files/lambda-function/Makefile @@ -0,0 +1,2 @@ +build-MartinFunction: + cp -a . $(ARTIFACTS_DIR) diff --git a/.github/files/lambda-function/config.yaml b/.github/files/lambda-function/config.yaml new file mode 100644 index 000000000..36cdfa5c9 --- /dev/null +++ b/.github/files/lambda-function/config.yaml @@ -0,0 +1,3 @@ +pmtiles: + sources: + webp2: ./webp2.pmtiles diff --git a/.github/files/lambda-layer/bootstrap b/.github/files/lambda-layer/bootstrap new file mode 100755 index 000000000..1b51ae350 --- /dev/null +++ b/.github/files/lambda-layer/bootstrap @@ -0,0 +1,3 @@ +#!/bin/sh +set -eu +exec martin -c "${_HANDLER}" diff --git a/.github/files/lambda.yaml b/.github/files/lambda.yaml new file mode 100644 index 000000000..7baaf448a --- /dev/null +++ b/.github/files/lambda.yaml @@ -0,0 +1,19 @@ +# This is a minimal AWS SAM template sufficient to invoke the handler +# in the CI environment. It probably hasn't been tested for actual +# deployments. + +AWSTemplateFormatVersion: 2010-09-09 +Transform: 'AWS::Serverless-2016-10-31' +Resources: + MartinLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: lambda-layer/ + MartinFunction: + Type: 'AWS::Serverless::Function' + Properties: + Runtime: provided.al2023 + Layers: + - Ref: MartinLayer + CodeUri: lambda-function/ + Handler: config.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cb120859..5e73ec00b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,6 +132,10 @@ jobs: with: install: true platforms: linux/amd64,linux/arm64 + - name: Set up AWS SAM + uses: aws-actions/setup-sam@v2 + with: + use-installer: true - name: Build targets run: | @@ -177,6 +181,7 @@ jobs: load: true tags: ${{ github.repository }}:linux-arm64 platforms: linux/arm64 + - name: Test linux/arm64 Docker image run: | PLATFORM=linux/arm64 @@ -288,6 +293,22 @@ jobs: name: build-${{ matrix.target }} path: target_releases/* + test-aws-lambda: + name: Test AWS Lambda + runs-on: ubuntu-latest + needs: [ build ] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Download build artifact cross-build + uses: actions/download-artifact@v3 + with: + name: cross-build + - run: tests/test-aws-lambda.sh + env: + MARTIN_BIN: x86_64-unknown-linux-musl/martin + + test-multi-os: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 80282051e..6dec1c8b4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pg_data/ config.yml tests/output/ tmp/ +.aws-sam/ diff --git a/Cargo.lock b/Cargo.lock index d88b13461..723c65d6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "brotli", "bytes", @@ -387,6 +387,28 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "async-trait" version = "0.1.77" @@ -438,6 +460,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -1920,6 +1948,55 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "lambda-web" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6277b60649250d681654162b7e8e875c938295ea5f883eb9a8da7e27d2c051" +dependencies = [ + "actix-http", + "actix-service", + "actix-web", + "base64 0.13.1", + "brotli", + "lambda_runtime", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "lambda_runtime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd32d5799db2155ae4d47116bb3e169b59f531ced4d5762a10c2125bdd2bf134" +dependencies = [ + "async-stream", + "bytes", + "futures", + "http", + "hyper", + "lambda_runtime_api_client", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tracing", +] + +[[package]] +name = "lambda_runtime_api_client" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7210012be904051520f0dc502140ba599bae3042b65b3737b87727f1aa88a7d6" +dependencies = [ + "http", + "hyper", + "tokio", + "tower-service", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2044,6 +2121,7 @@ dependencies = [ "insta", "itertools 0.12.1", "json-patch", + "lambda-web", "log", "martin-tile-utils", "mbtiles", @@ -2478,7 +2556,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" dependencies = [ - "base64", + "base64 0.21.7", "serde", ] @@ -2521,6 +2599,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -2653,7 +2751,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" dependencies = [ - "base64", + "base64 0.21.7", "byteorder", "bytes", "fallible-iterator 0.2.0", @@ -3005,7 +3103,7 @@ version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -3249,7 +3347,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -3258,7 +3356,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" dependencies = [ - "base64", + "base64 0.21.7", "rustls-pki-types", ] @@ -3457,7 +3555,7 @@ version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b0ed1662c5a68664f45b76d18deb0e234aff37207086803165c961eb695e981" dependencies = [ - "base64", + "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", @@ -3784,7 +3882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "byteorder", "bytes", @@ -3826,7 +3924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "byteorder", "crc", @@ -4305,6 +4403,27 @@ dependencies = [ "serde", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -4495,7 +4614,7 @@ version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51daa774fe9ee5efcf7b4fec13019b8119cda764d9a8b5b06df02bb1445c656" dependencies = [ - "base64", + "base64 0.21.7", "log", "pico-args", "usvg-parser", diff --git a/Cargo.toml b/Cargo.toml index deb53b7c0..daf579550 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ indoc = "2" insta = "1" itertools = "0.12" json-patch = "1.2" +lambda-web = { version = "0.2.1", features = ["actix4"] } log = "0.4" martin-tile-utils = { path = "./martin-tile-utils", version = "0.4.0" } mbtiles = { path = "./mbtiles", version = "0.9.0" } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index ccb584208..bd7d57b55 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -7,6 +7,7 @@ - [Running with Docker](run-with-docker.md) - [Running with Docker Compose](run-with-docker-compose.md) - [Running with NGINX](run-with-nginx.md) + - [Running in AWS Lambda](run-with-lambda.md) - [Troubleshooting](troubleshooting.md) - [Configuration File](config-file.md) - [PostgreSQL Connections](pg-connections.md) diff --git a/docs/src/env-vars.md b/docs/src/env-vars.md index 0c9ef7536..0bb98a9e9 100644 --- a/docs/src/env-vars.md +++ b/docs/src/env-vars.md @@ -9,3 +9,4 @@ You can also configure Martin using environment variables, but only if the confi | `PGSSLCERT`
`ssl_cert` | `./postgresql.crt` | A file with a client SSL certificate. [docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT) | | `PGSSLKEY`
`ssl_key` | `./postgresql.key` | A file with the key for the client SSL certificate. [docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY) | | `PGSSLROOTCERT`
`ssl_root_cert` | `./root.crt` | A file with trusted root certificate(s). The file should contain a sequence of PEM-formatted CA certificates. [docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT) | +| `AWS_LAMBDA_RUNTIME_API` | | If defined, connect to AWS Lambda to handle requests. The regular HTTP server is not used. See [Running in AWS Lambda](run-with-lambda.md) | diff --git a/docs/src/run-with-lambda.md b/docs/src/run-with-lambda.md new file mode 100644 index 000000000..59641418a --- /dev/null +++ b/docs/src/run-with-lambda.md @@ -0,0 +1,131 @@ +## Using with AWS Lambda + +Martin can be run in AWS Lambda. This is useful if you want to serve tiles from a serverless environment, while accessing "nearby" data from a PostgreSQL database or PMTiles file in S3, without exposing the raw file to the world to prevent download abuse and improve performance. + +Some very brief context: Lambda has two deployment models, zip file and container-based. When using zip file deployment, the online code editor is available, in which we can edit the .yaml configuration. When using container-based deployment, we can pass our configuration on the command line or environment variables. + +Everything can be performed from AWS CloudShell, otherwise you will need to install the AWS CLI and the AWS SAM CLI, and configure authentication. The CloudShell also runs in a particular AWS region. + +### Container deployment + +Lambda images must come from a public or private ECR registry. Pull the image from GHCR and push it to ECR. + +```bash +$ docker pull ghcr.io/maplibre/martin:latest --platform linux/arm64 +$ aws ecr create-repository --repository-name martin +[…] + "repositoryUri": "493749042871.dkr.ecr.us-east-2.amazonaws.com/martin", +# Read the repositoryUri which includes your account number +$ docker tag ghcr.io/maplibre/martin:latest 493749042871.dkr.ecr.us-east-2.amazonaws.com/martin:latest +$ aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 493749042871.dkr.ecr.us-east-2.amazonaws.com +$ docker push 493749042871.dkr.ecr.us-east-2.amazonaws.com/martin:latest +``` + +Now you can go to the [Lambda console](https://console.aws.amazon.com/lambda) and create your function. + +1. Click “Create function”. +2. Choose “Container image”. +3. Put something in “Function name”. (Note: This is an internal identifier, not exposed in the function URL.) +4. Click “Browse images”, and select your repository and the tag. (If you can’t find it, see if you’re in the same region?) +5. Expand “Container image overrides”, and under CMD put the URL of a .pmtiles file. +6. Set “Architecture” to arm64 to match the platform that we pulled. (Lambda has better ARM CPUs than x86.) +7. Click “Create function”. +8. Find the “Configuration” tab, select “Function URL”, “Create function URL”. +9. Set “Auth type” to `NONE` + * Do not enable CORS. Martin already has CORS support, so it will create duplicate headers and break CORS. +10. Click on the “Function URL”. If it works, hooray! If it doesn’t, open the “Monitor” tab, “View CloudWatch logs”, find the most recent Log stream. + +### Zip deployment + +It’s possible to deploy the entire codebase from the AWS console, but we will use Serverless Application Model. Our function will consist of a “Layer”, containing the Martin binary, and our function itself will contain the configuration in .yaml format. + +#### The layer + +Download the binary and place it in your staging directory. The `bin` directory of your Layer will be added to the PATH. + +```bash +mkdir -p martin_layer/src/bin/ +cd martin_layer +curl -OL https://github.com/maplibre/martin/releases/latest/download/martin-aarch64-unknown-linux-musl.tar.gz +tar -C src/bin/ -xzf martin-aarch64-unknown-linux-musl.tar.gz martin +``` + +Every zip-based Lambda function runs a file called `bootstrap`. + +```bash +cat <src/bootstrap +#!/bin/sh +set -eu +exec martin -c ${_HANDLER}.yaml +EOF +``` + +Write the SAM template. + +```yaml +cat <template.yaml +AWSTemplateFormatVersion: 2010-09-09 +Transform: 'AWS::Serverless-2016-10-31' +Resources: + martin: + Type: 'AWS::Serverless::LayerVersion' + DeletionPolicy: Delete + Properties: + ContentUri: src + CompatibleRuntimes: + - provided.al2023 + CompatibleArchitectures: + - arm64 +Outputs: + LayerArn: + Value: !Ref MartinLayer + Export: + Name: !Sub "${AWS::StackName}-LayerArn" +EOF +``` + +Run `sam deploy --guided`. + +1. Stack Name: Name your CloudFormation stack something like `martin-layer`. +2. Press enter for everything else +3. The settings are saved to `samconfig.toml`, so you can later do `sam deploy` to update the version, or `sam delete`. + +Now if you visit the [Lambda console](https://console.aws.amazon.com/lambda/home) and select “Layers”, you should see your layer. + +#### The function + +1. Select “Functions”, “Create function”. +2. Put something in “Function name”. +3. Set “Runtime” to “Amazon Linux 2023”. +4. Set “Architecture” to “arm64”. +5. Under “Advanced settings”, choose “Enable function URL” with “Auth type” of “NONE”. +6. Click “Create function”. + +Add your layer: + +1. Click “add a layer” (green banner at the top, or the very bottom). +2. Choose “Custom layers”, and select your layer and its version. +3. Click “Add”. + +Add your configuration file in the function source code: + +1. Code tab, File, New File: `hello.handler.yaml`. + + ```yaml + pmtiles: + sources: + demotiles: + ``` + +2. Click Deploy, wait for the success banner, and visit your function URL. + +### TODO + +This support is preliminary; there are features to add to Martin, configuration to tweak, and documentation to write. + +* Lambda has a default timeout of 3 seconds, and 128 MB of memory, maybe this is suboptimal. +* Document how to connect to a PostgreSQL database on RDS. +* Set up a CloudFront CDN, this is a whole thing, but explain the motivation and the basics. +* Grant the execution role permission to read objects from an S3 bucket, and teach Martin how to make authenticated requests to S3. +* Teach Martin how to serve all PMTiles files from an S3 bucket rather than having to list them at startup. +* Teach Martin how to set the Cache-Control and Etag headers for better defaults. diff --git a/justfile b/justfile index f92dc9bf1..ac5576d69 100644 --- a/justfile +++ b/justfile @@ -177,6 +177,10 @@ test-int: clean-test install-sqlx fi fi +# Run AWS Lambda smoke test against SAM local +test-lambda: + tests/test-aws-lambda.sh + # Run integration tests and save its output as the new expected output bless: restart clean-test bless-insta-martin bless-insta-mbtiles bless-tests bless-int diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 24a778ca7..91d3f0a6f 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -60,8 +60,9 @@ name = "bench" harness = false [features] -default = ["fonts", "mbtiles", "pmtiles", "postgres", "sprites"] +default = ["fonts", "lambda", "mbtiles", "pmtiles", "postgres", "sprites"] fonts = ["dep:bit-set", "dep:pbf_font_tools"] +lambda = ["dep:lambda-web"] mbtiles = ["dep:mbtiles"] pmtiles = ["dep:pmtiles", "dep:reqwest"] postgres = ["dep:deadpool-postgres", "dep:json-patch", "dep:postgis", "dep:postgres", "dep:postgres-protocol", "dep:semver", "dep:tokio-postgres-rustls"] @@ -110,6 +111,7 @@ tilejson.workspace = true tokio = { workspace = true, features = ["io-std"] } tokio-postgres-rustls = { workspace = true, optional = true } url.workspace = true +lambda-web = { workspace = true, optional = true } [dev-dependencies] cargo-husky.workspace = true diff --git a/martin/src/bin/martin-cp.rs b/martin/src/bin/martin-cp.rs index feecbfc95..d24031ea9 100644 --- a/martin/src/bin/martin-cp.rs +++ b/martin/src/bin/martin-cp.rs @@ -430,19 +430,15 @@ async fn main() { let env = env_logger::Env::default().default_filter_or("martin_cp=info"); env_logger::Builder::from_env(env).init(); - start(CopierArgs::parse()) - .await - .unwrap_or_else(|e| on_error(e)); -} - -fn on_error(e: E) -> ! { - // Ensure the message is printed, even if the logging is disabled - if log_enabled!(log::Level::Error) { - error!("{e}"); - } else { - eprintln!("{e}"); + if let Err(e) = start(CopierArgs::parse()).await { + // Ensure the message is printed, even if the logging is disabled + if log_enabled!(log::Level::Error) { + error!("{e}"); + } else { + eprintln!("{e}"); + } + std::process::exit(1); } - std::process::exit(1); } #[cfg(test)] diff --git a/martin/src/bin/martin.rs b/martin/src/bin/martin.rs index 054970e49..d54adeba5 100644 --- a/martin/src/bin/martin.rs +++ b/martin/src/bin/martin.rs @@ -1,6 +1,3 @@ -use std::fmt::Display; - -use actix_web::dev::Server; use clap::Parser; use log::{error, info, log_enabled}; use martin::args::{Args, OsEnv}; @@ -9,7 +6,7 @@ use martin::{read_config, Config, MartinResult}; const VERSION: &str = env!("CARGO_PKG_VERSION"); -async fn start(args: Args) -> MartinResult { +async fn start(args: Args) -> MartinResult<()> { info!("Starting Martin v{VERSION}"); let env = OsEnv::default(); @@ -35,8 +32,7 @@ async fn start(args: Args) -> MartinResult { let (server, listen_addresses) = new_server(config.srv, sources)?; info!("Martin has been started on {listen_addresses}."); info!("Use http://{listen_addresses}/catalog to get the list of available sources."); - - Ok(server) + server.await } #[actix_web::main] @@ -44,19 +40,13 @@ async fn main() { let env = env_logger::Env::default().default_filter_or("martin=info"); env_logger::Builder::from_env(env).init(); - start(Args::parse()) - .await - .unwrap_or_else(|e| on_error(e)) - .await - .unwrap_or_else(|e| on_error(e)); -} - -fn on_error(e: E) -> ! { - // Ensure the message is printed, even if the logging is disabled - if log_enabled!(log::Level::Error) { - error!("{e}"); - } else { - eprintln!("{e}"); + if let Err(e) = start(Args::parse()).await { + // Ensure the message is printed, even if the logging is disabled + if log_enabled!(log::Level::Error) { + error!("{e}"); + } else { + eprintln!("{e}"); + } + std::process::exit(1); } - std::process::exit(1); } diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 79d87262e..cf89cc605 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -1,13 +1,17 @@ +use std::future::Future; +use std::pin::Pin; use std::string::ToString; use std::time::Duration; use actix_cors::Cors; -use actix_web::dev::Server; use actix_web::error::ErrorInternalServerError; use actix_web::http::header::CACHE_CONTROL; use actix_web::middleware::TrailingSlash; use actix_web::web::Data; use actix_web::{middleware, route, web, App, HttpResponse, HttpServer, Responder}; +use futures::TryFutureExt; +#[cfg(feature = "lambda")] +use lambda_web::{is_running_on_lambda, run_actix_on_lambda}; use log::error; use serde::{Deserialize, Serialize}; @@ -98,16 +102,13 @@ pub fn router(cfg: &mut web::ServiceConfig) { cfg.service(crate::srv::fonts::get_font); } -/// Create a new initialized Actix `App` instance together with the listening address. +type Server = Pin>>>; + +/// Create a future for an Actix web server together with the listening address. pub fn new_server(config: SrvConfig, state: ServerState) -> MartinResult<(Server, String)> { let catalog = Catalog::new(&state)?; - let keep_alive = Duration::from_secs(config.keep_alive.unwrap_or(KEEP_ALIVE_DEFAULT)); - let worker_processes = config.worker_processes.unwrap_or_else(num_cpus::get); - let listen_addresses = config - .listen_addresses - .unwrap_or_else(|| LISTEN_ADDRESSES_DEFAULT.to_owned()); - let server = HttpServer::new(move || { + let factory = move || { let cors_middleware = Cors::default() .allow_any_origin() .allowed_methods(vec!["GET"]); @@ -127,15 +128,29 @@ pub fn new_server(config: SrvConfig, state: ServerState) -> MartinResult<(Server .wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly)) .wrap(middleware::Logger::default()) .configure(router) - }) - .bind(listen_addresses.clone()) - .map_err(|e| BindingError(e, listen_addresses.clone()))? - .keep_alive(keep_alive) - .shutdown_timeout(0) - .workers(worker_processes) - .run(); - - Ok((server, listen_addresses)) + }; + + #[cfg(feature = "lambda")] + if is_running_on_lambda() { + let server = run_actix_on_lambda(factory).err_into(); + return Ok((Box::pin(server), "(aws lambda)".into())); + } + + let keep_alive = Duration::from_secs(config.keep_alive.unwrap_or(KEEP_ALIVE_DEFAULT)); + let worker_processes = config.worker_processes.unwrap_or_else(num_cpus::get); + let listen_addresses = config + .listen_addresses + .unwrap_or_else(|| LISTEN_ADDRESSES_DEFAULT.to_owned()); + + let server = HttpServer::new(factory) + .bind(listen_addresses.clone()) + .map_err(|e| BindingError(e, listen_addresses.clone()))? + .keep_alive(keep_alive) + .shutdown_timeout(0) + .workers(worker_processes) + .run() + .err_into(); + Ok((Box::pin(server), listen_addresses)) } #[cfg(test)] diff --git a/martin/src/utils/error.rs b/martin/src/utils/error.rs index 989a078bb..2843eb452 100644 --- a/martin/src/utils/error.rs +++ b/martin/src/utils/error.rs @@ -80,6 +80,9 @@ pub enum MartinError { #[error(transparent)] WebError(#[from] actix_web::Error), + #[error(transparent)] + IoError(#[from] io::Error), + #[error("Internal error: {0}")] - InternalError(Box), + InternalError(#[from] Box), } diff --git a/tests/test-aws-lambda.sh b/tests/test-aws-lambda.sh new file mode 100755 index 000000000..7ac710a67 --- /dev/null +++ b/tests/test-aws-lambda.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +have () { + hash -- "$1" 2>&- +} + +if ! have sam; then + echo "The AWS Serverless Application Model Command Line Interface (AWS SAM CLI) " + echo "must be installed: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html" + exit 1 +fi + +# Just send a single request using `sam local invoke` to verify that +# the server boots, finds a source to serve, and can handle a request. +# TODO Run the fuller integration suite against this. +# In doing so, switch from `sam local invoke`, which starts and stops the +# server, to `sam local start-api`, which keeps it running. + +EVENT=$(sam local generate-event apigateway http-api-proxy \ + | jq '.rawPath="/"|.requestContext.http.method="GET"') + +# `sam build` will copy the _entire_ context to a temporary directory, +# so just give it the files we need +mkdir -p .github/files/lambda-layer/bin/ +if ! install ${MARTIN_BIN:-target/debug/martin} .github/files/lambda-layer/bin/; then + echo "Specify the binary, e.g. ‘MARTIN_BIN=target/x86_64-linux-unknown-musl/release/martin just test-lambda’" + exit 1 +fi +cp ./tests/fixtures/pmtiles2/webp2.pmtiles .github/files/lambda-function/ +sam build -t .github/files/lambda.yaml + +echo "$EVENT" | sam local invoke -e - \ + | jq -ne 'input.statusCode==200'