diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a56e645 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +{ + "name": "Lambda Rust HTTP", + "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", + "features": { + "ghcr.io/devcontainers/features/aws-cli:1": {}, + "ghcr.io/devcontainers/features/terraform:1": {}, + "ghcr.io/jajera/features/zip:1": {}, + "ghcr.io/devcontainers-extra/features/zig:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "ms-vscode.vscode-json", + "hashicorp.terraform" + ] + } + }, + "postCreateCommand": "cargo install cargo-lambda", + "remoteUser": "vscode" +} diff --git a/.github/workflows/commitmsg-conform.yml b/.github/workflows/commitmsg-conform.yml new file mode 100644 index 0000000..84b9661 --- /dev/null +++ b/.github/workflows/commitmsg-conform.yml @@ -0,0 +1,11 @@ +name: commit-message-conformance +on: + pull_request: {} +permissions: + statuses: write + checks: write + contents: read + pull-requests: read +jobs: + commitmsg-conform: + uses: actionsforge/actions/.github/workflows/commitmsg-conform.yml@main \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..6eaa86d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,185 @@ +name: Build and Deploy Rust Lambda + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +# Cancel in-progress jobs when new commits are pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FUNCTION_NAME: lambda_http_geolocation + RUNTIME: provided.al2023 + ARCHITECTURE: arm64 + +permissions: + contents: write + packages: write + +jobs: + validate: + name: Validate and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: aarch64-unknown-linux-gnu + override: true + + - name: Cache Zig + uses: actions/cache@v4 + with: + path: | + ~/.zig + zig-linux-x86_64-0.13.0 + key: ${{ runner.os }}-zig-0.13.0 + + - name: Install Zig + run: | + if [ ! -d "zig-linux-x86_64-0.13.0" ]; then + curl -L https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar -xJ + fi + echo "$PWD/zig-linux-x86_64-0.13.0" >> $GITHUB_PATH + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + ~/.cargo/bin + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install cargo-lambda + run: | + if ! command -v cargo-lambda &> /dev/null; then + cargo install cargo-lambda + else + echo "✅ cargo-lambda already installed" + fi + + - name: Run clippy + run: | + cargo clippy --all-targets --all-features -- -D warnings + + - name: Run tests + run: | + cargo test + + - name: Build Lambda function + run: | + cargo lambda build --release --arm64 + + - name: Verify binary + run: | + if [ ! -f "target/lambda/lambda_http_geolocation/bootstrap" ]; then + echo "❌ Binary not found at target/lambda/lambda_http_geolocation/bootstrap" + exit 1 + fi + echo "✅ Binary created successfully" + ls -la target/lambda/lambda_http_geolocation/bootstrap + file target/lambda/lambda_http_geolocation/bootstrap + + release: + name: Create Release + runs-on: ubuntu-latest + needs: validate + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: aarch64-unknown-linux-gnu + override: true + + - name: Cache Zig + uses: actions/cache@v4 + with: + path: | + ~/.zig + zig-linux-x86_64-0.13.0 + key: ${{ runner.os }}-zig-0.13.0 + + - name: Install Zig + run: | + if [ ! -d "zig-linux-x86_64-0.13.0" ]; then + curl -L https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar -xJ + fi + echo "$PWD/zig-linux-x86_64-0.13.0" >> $GITHUB_PATH + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + ~/.cargo/bin + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install cargo-lambda + run: | + if ! command -v cargo-lambda &> /dev/null; then + cargo install cargo-lambda + else + echo "✅ cargo-lambda already installed" + fi + + - name: Build Lambda function + run: | + cargo lambda build --release --arm64 + + - name: Create deployment package + run: | + cd target/lambda/lambda_http_geolocation + zip -r lambda_http_geolocation.zip bootstrap + cd ../../.. + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: target/lambda/lambda_http_geolocation/lambda_http_geolocation.zip + tag_name: v${{ github.run_number }} + name: Release v${{ github.run_number }} + body: | + ## Rust Lambda Function Release + + **Function:** ${{ env.FUNCTION_NAME }} + **Runtime:** ${{ env.RUNTIME }} + **Architecture:** ${{ env.ARCHITECTURE }} + + ### Installation + 1. Download the `lambda_http_geolocation.zip` file + 2. Upload to AWS Lambda console or use AWS CLI + 3. Set handler to: `bootstrap` + 4. Set runtime to: `provided.al2023` + 5. Set architecture to: `arm64` + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag Latest + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -f latest + git push origin latest --force \ No newline at end of file diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml new file mode 100644 index 0000000..6ed56d5 --- /dev/null +++ b/.github/workflows/markdown-lint.yml @@ -0,0 +1,11 @@ +name: markdown-lint +on: + pull_request: {} +permissions: + statuses: write + checks: write + contents: read + pull-requests: read +jobs: + markdown-lint: + uses: actionsforge/actions/.github/workflows/markdown-lint.yml@main \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad67955..b652975 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,39 @@ -# Generated by Cargo -# will have compiled files and executables -debug -target - -# These are backup files generated by rustfmt +# Rust +/target/ **/*.rs.bk +Cargo.lock + +# Lambda build artifacts +*.zip +target/lambda/ + +# Terraform +*.tfstate +*.tfstate.* +.terraform/ +.terraform.lock.hcl +tfplan + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb +# Environment variables +.env +.env.local -# Generated by cargo mutants -# Contains mutation testing data -**/mutants.out*/ +# AWS +.aws/ -# RustRover -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# Temporary files +*.tmp +*.temp diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..769a649 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "lambda_http_geolocation" +version = "0.1.0" +edition = "2021" + +[package.metadata.lambda] +runtime = "provided.al2023" +architecture = "arm64" + +[dependencies] +lambda_http = "0.9" +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false } +tracing = "0.1" +tracing-subscriber = { version = "0.3", optional = true } + +[features] +default = ["dev-tracing"] +dev-tracing = ["tracing-subscriber"] + +[dev-dependencies] + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +[profile.dev] +opt-level = 0 +debug = true diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..7c3e99c --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,84 @@ +# Quick Start Guide + +## Prerequisites + +- Docker and Docker Compose +- VS Code with Dev Containers extension +- AWS CLI configured (optional for local development) + +## Getting Started + +### 1. Open in Dev Container + +1. Clone this repository +2. Open in VS Code +3. When prompted, click "Reopen in Container" +4. Wait for the container to build (this may take a few minutes) + +### 2. Verify Setup + +The devcontainer automatically includes: + +- ✅ Rust toolchain (1.70+) +- ✅ AWS CLI +- ✅ Terraform +- ✅ Zig (for ARM64 cross-compilation) +- ✅ zip utilities +- ✅ cargo-lambda (installed automatically) + +### 3. Build and Test + +```bash +# Setup Rust targets +./setup.sh + +# Build the Lambda function +./build.sh + +# Test locally +cargo lambda watch + +# In another terminal, test the API +curl -X POST "http://localhost:9000/2015-03-31/functions/function/invocations" \ + -d '{"httpMethod": "GET", "path": "/geo", "queryStringParameters": {"ip": "8.8.8.8"}}' +``` + +### 4. Deploy to AWS + +```bash +# Deploy everything (builds + infrastructure) +cd infra +terraform init +terraform apply +``` + +## Project Structure + +```plaintext +lambda_http_geolocation/ +├── .devcontainer/ # VS Code devcontainer config +│ └── devcontainer.json # Container settings with pre-built image +├── src/ # Rust source code +│ └── main.rs # Lambda function +├── infra/ # Terraform infrastructure +│ ├── main.tf # Main configuration +│ ├── variables.tf # Variables +│ └── outputs.tf # Outputs +├── setup.sh # Development environment setup +├── build.sh # Build and package script +├── Cargo.toml # Rust dependencies +└── README.md # Full documentation +``` + +## Next Steps + +- Read the full [README.md](README.md) for detailed documentation +- Customize the Lambda function in `src/main.rs` +- Modify infrastructure in `infra/` directory +- Add tests and CI/CD pipelines + +## Troubleshooting + +- **Container won't start**: Ensure Docker is running and has enough resources +- **Build fails**: Check that cargo-lambda is installed in the container +- **Deployment fails**: Verify AWS credentials and permissions diff --git a/README.md b/README.md index 80613b5..4cf8ed6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,198 @@ -# lambda-rust-http-geolocation -Rust-based AWS Lambda API that looks up client IP geolocation using a free external API +# Lambda HTTP Geolocation + +A minimal AWS Lambda example in Rust that provides geolocation information for IP addresses via API Gateway, with developer-friendly Devcontainer setup. + +## 🚀 Quick Start + +### Prerequisites + +- Docker Desktop +- VS Code with Dev Containers extension +- AWS CLI configured with appropriate credentials + +### 1. Open in Dev Container + +1. Clone this repository +2. Open in VS Code +3. When prompted, click "Reopen in Container" +4. Wait for the container to build and install dependencies + +The devcontainer automatically includes: + +- ✅ Rust toolchain (1.70+) +- ✅ AWS CLI +- ✅ Terraform +- ✅ Zig (for ARM64 cross-compilation) +- ✅ zip utilities +- ✅ cargo-lambda (installed automatically) + +### 2. Setup Development Environment + +```bash +# Run the setup script to install Rust targets +./setup.sh +``` + +### 3. Local Development + +```bash +# Run all local tests (compilation, clippy, tests) +./test.sh + +# Watch for changes and run locally +cargo lambda watch + +# In another terminal, test locally (Lambda runtime format) +curl "http://localhost:9000/2015-03-31/functions/lambda_http_geolocation/invocations" \ + -X POST \ + -d '{"version":"2.0","routeKey":"GET /geo","rawPath":"/geo","rawQueryString":"ip=8.8.8.8","queryStringParameters":{"ip":"8.8.8.8"},"requestContext":{"http":{"method":"GET","path":"/geo"}},"body":"","isBase64Encoded":false}' + +# After deployment, the API is much simpler: +# curl "https://your-api-gateway-url/prod/geo?ip=8.8.8.8" +``` + +### 4. Build for Production + +```bash +# Build ARM64 binary for AWS Lambda +./build.sh + +# The binary will be created at: +# target/lambda/lambda_http_geolocation/bootstrap +``` + +### 5. Deploy to AWS + +```bash +# Navigate to infrastructure directory +cd infra + +# Initialize Terraform +terraform init + +# Plan deployment +terraform plan + +# Deploy infrastructure +terraform apply + +# Get the API Gateway URL +terraform output api_gateway_url +``` + +### 6. Test the Deployed Function + +```bash +# Test with the URL from terraform output +curl "https://.execute-api..amazonaws.com/prod/geo?ip=8.8.8.8" +# Response: {"country":"United States","city":"Mountain View",...} +``` + +## 🏗️ Project Structure + +```plaintext +lambda_http_geolocation/ +├── .devcontainer/ # VS Code Dev Container config +│ └── devcontainer.json # Container settings with pre-built image +├── src/ +│ └── main.rs # Lambda function code +├── infra/ # Terraform infrastructure +│ ├── main.tf # Main infrastructure config +│ ├── variables.tf # Variable definitions +│ └── outputs.tf # Output values +├── setup.sh # Development environment setup +├── build.sh # Build and package script +├── Cargo.toml # Rust dependencies +└── README.md # This file +``` + +## 🌐 API Endpoints + +- **Help**: `GET /` → Shows API usage information and examples +- **Geolocation**: `GET /geo` → Returns geolocation data for IP address +- **Query Parameters**: + - `ip` (optional): Specific IP address to lookup. If not provided, uses the client's IP from API Gateway. +- **Invalid Endpoints**: Any non-existent endpoint will return the help information + +**Response Format:** + +```json +{ + "country": "United States", + "country_code": "US", + "region": "CA", + "city": "Mountain View", + "lat": 37.4056, + "lon": -122.0775, + "timezone": "America/Los_Angeles", + "isp": "Google LLC", + "org": "Google Public DNS" +} +``` + +## 🔧 Development Workflow + +1. **Setup**: Run `./setup.sh` to install Rust targets +2. **Local Development**: Use `cargo lambda watch` for hot reloading +3. **Testing**: Test locally before deploying +4. **Build**: Use `./build.sh` for production build +5. **Deploy**: Use Terraform to deploy infrastructure +6. **Test**: Verify the deployed function works + +## 📦 Build Artifacts + +The build process creates: + +- **Binary**: `target/lambda/lambda_http_geolocation/bootstrap` (ARM64) +- **Package**: `target/lambda/lambda_http_geolocation/lambda_http_geolocation.zip` (for Terraform) + +## 🛠️ Technologies Used + +- **Rust**: Programming language +- **lambda_http**: AWS Lambda HTTP runtime +- **cargo-lambda**: Build tool for Rust Lambda functions +- **reqwest**: HTTP client for external API calls +- **serde**: JSON serialization/deserialization +- **Terraform**: Infrastructure as Code +- **Dev Containers**: Consistent development environment with pre-built image +- **Zig**: Cross-compilation tool for ARM64 builds (included in devcontainer) + +## 📚 Reference + +This implementation provides geolocation services using the free [ip-api.com](http://ip-api.com/) API and follows AWS Lambda best practices. The infrastructure uses API Gateway REST API with proxy integration for flexible routing. + +## 🔍 Troubleshooting + +### Common Issues + +1. **Build fails**: Ensure you're using the Dev Container with Rust 1.70+ +2. **Terraform errors**: Check AWS credentials and region configuration +3. **Lambda cold start**: First invocation may be slower +4. **Geolocation API errors**: Check if ip-api.com is accessible + +### Useful Commands + +```bash +# Check Rust version +rustc --version + +# Check cargo-lambda installation +cargo lambda --version + +# Check AWS CLI +aws --version + +# Check Terraform +terraform --version + +# Check Zig +zig version + +# Setup and build +./setup.sh +./build.sh +``` + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..55232c5 --- /dev/null +++ b/build.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e + +echo "🚀 Building Lambda HTTP Geolocation function..." + +# Check if cargo-lambda is installed +if ! command -v cargo-lambda &> /dev/null; then + echo "❌ cargo-lambda not found. Installing..." + cargo install cargo-lambda +fi + +# Run clippy checks first +echo "🔍 Running clippy checks..." +if ! cargo clippy -- -D warnings; then + echo "❌ Clippy checks failed! Fix warnings before building." + exit 1 +fi +echo "✅ Clippy checks passed" + +# Clean previous builds +echo "🧹 Cleaning previous builds..." +cargo clean + +# Build the release binary +echo "🔨 Building ARM64 binary..." +cargo lambda build --release --arm64 + +# Check if binary was created +if [ ! -f "target/lambda/lambda_http_geolocation/bootstrap" ]; then + echo "❌ Binary not found at target/lambda/lambda_http_geolocation/bootstrap" + exit 1 +fi + +echo "✅ Build successful!" +echo "📦 Binary location: target/lambda/lambda_http_geolocation/bootstrap" +echo "📏 Binary size: $(du -h target/lambda/lambda_http_geolocation/bootstrap | cut -f1)" + +# Create deployment package +echo "📦 Creating deployment package..." +cd target/lambda/lambda_http_geolocation +zip -r lambda_http_geolocation.zip bootstrap +cd ../../.. + +echo "🎉 Deployment package ready at: target/lambda/lambda_http_geolocation/lambda_http_geolocation.zip" +echo "📏 Package size: $(du -h target/lambda/lambda_http_geolocation/lambda_http_geolocation.zip | cut -f1)" +echo "" +echo "🚀 Ready to deploy with Terraform!" +echo " cd infra && terraform apply" diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..ab17624 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,124 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +# Archive the Lambda function code +data "archive_file" "lambda_zip" { + type = "zip" + source_file = "../target/lambda/lambda_http_geolocation/bootstrap" + output_path = "../target/lambda/lambda_http_geolocation.zip" +} + +# IAM role for Lambda execution +resource "aws_iam_role" "lambda_exec" { + name = "lambda_http_geolocation-exec-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +# Attach basic execution role policy +resource "aws_iam_role_policy_attachment" "lambda_basic" { + role = aws_iam_role.lambda_exec.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +# Lambda function +resource "aws_lambda_function" "main" { + filename = data.archive_file.lambda_zip.output_path + function_name = "lambda_http_geolocation" + role = aws_iam_role.lambda_exec.arn + handler = "bootstrap" + source_code_hash = data.archive_file.lambda_zip.output_base64sha256 + runtime = "provided.al2023" + architectures = ["arm64"] + + environment { + variables = { + RUST_LOG = "info" + } + } +} + +# API Gateway V2 (HTTP API) +resource "aws_apigatewayv2_api" "main" { + name = "lambda_http_geolocation-api" + protocol_type = "HTTP" + + cors_configuration { + allow_origins = ["*"] + allow_methods = ["GET", "OPTIONS"] + allow_headers = ["Content-Type"] + } +} + +# API Gateway V2 stage +resource "aws_apigatewayv2_stage" "main" { + api_id = aws_apigatewayv2_api.main.id + name = "prod" + auto_deploy = true +} + +# API Gateway V2 integration +resource "aws_apigatewayv2_integration" "main" { + api_id = aws_apigatewayv2_api.main.id + integration_type = "AWS_PROXY" + + integration_method = "POST" + integration_uri = aws_lambda_function.main.invoke_arn + payload_format_version = "2.0" +} + +# API Gateway V2 route for root +resource "aws_apigatewayv2_route" "root" { + api_id = aws_apigatewayv2_api.main.id + route_key = "GET /" + target = "integrations/${aws_apigatewayv2_integration.main.id}" +} + +# API Gateway V2 route for geolocation +resource "aws_apigatewayv2_route" "geo" { + api_id = aws_apigatewayv2_api.main.id + route_key = "GET /geo" + target = "integrations/${aws_apigatewayv2_integration.main.id}" +} + +# API Gateway V2 route for catch-all (proxy) +resource "aws_apigatewayv2_route" "proxy" { + api_id = aws_apigatewayv2_api.main.id + route_key = "GET /{proxy+}" + target = "integrations/${aws_apigatewayv2_integration.main.id}" +} + +# Lambda permission for API Gateway V2 +resource "aws_lambda_permission" "apigw" { + statement_id = "AllowExecutionFromAPIGateway" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.main.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*" +} diff --git a/infra/outputs.tf b/infra/outputs.tf new file mode 100644 index 0000000..2ec6e21 --- /dev/null +++ b/infra/outputs.tf @@ -0,0 +1,14 @@ +output "api_gateway_url" { + description = "URL of the API Gateway endpoint" + value = aws_apigatewayv2_stage.main.invoke_url +} + +output "lambda_function_arn" { + description = "ARN of the Lambda function" + value = aws_lambda_function.main.arn +} + +output "lambda_function_name" { + description = "Name of the Lambda function" + value = aws_lambda_function.main.function_name +} diff --git a/infra/variables.tf b/infra/variables.tf new file mode 100644 index 0000000..47be647 --- /dev/null +++ b/infra/variables.tf @@ -0,0 +1,5 @@ +variable "aws_region" { + description = "AWS region to deploy resources" + type = string + default = "ap-southeast-2" +} diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..5355fe4 --- /dev/null +++ b/setup.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Setup script for Rust Lambda ARM64 builds - lambda_http_geolocation +set -e + +echo "🚀 Setting up Rust Lambda development environment for lambda_http_geolocation..." + +# Check if ARM64 target is installed +if ! rustup target list --installed | grep -q "aarch64-unknown-linux-gnu"; then + echo "🎯 Installing ARM64 Rust target..." + rustup target add aarch64-unknown-linux-gnu + echo "✅ ARM64 target installed" +else + echo "✅ ARM64 target already installed" +fi + +# Verify cargo-lambda setup +echo "🔍 Verifying cargo-lambda setup..." +cargo lambda system + +echo "" +echo "🎉 Setup complete! You can now run:" +echo " cargo lambda build --release --arm64" +echo "" +echo "🚀 After building, deploy with:" +echo " cd infra && terraform apply" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..380d67e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,160 @@ +use lambda_http::{run, service_fn, Body, Error, Request, RequestExt, Response}; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Debug, Serialize, Deserialize)] +struct GeoResponse { + country: Option, + country_code: Option, + region: Option, + region_name: Option, + city: Option, + zip: Option, + lat: Option, + lon: Option, + timezone: Option, + isp: Option, + org: Option, + as_field: Option, + query: Option, + status: Option, + message: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ErrorResponse { + error: String, + message: String, +} + +async fn get_geolocation(ip: &str) -> Result> { + let url = format!("http://ip-api.com/json/{ip}"); + let response = reqwest::get(&url).await?; + let geo_data: GeoResponse = response.json().await?; + Ok(geo_data) +} + +async fn function_handler(request: Request) -> Result, Error> { + // Get the request path + let path = request.uri().path(); + + // Handle both local development and production paths + // Local: /geo, Production: /prod/geo, /v1/geo, etc. + if path.ends_with("/geo") { + handle_geolocation(request).await + } else { + handle_help().await // Any other endpoint returns help + } +} + +async fn handle_help() -> Result, Error> { + let help_content = json!({ + "message": "IP Geolocation API", + "description": "Get geolocation information for IP addresses", + "endpoints": { + "/geo": "Get geolocation data for an IP address", + "/": "Show this help information" + }, + "usage": { + "geolocation": "GET /geo?ip=8.8.8.8", + "examples": [ + "GET /geo?ip=8.8.8.8", + "GET /geo?ip=1.1.1.1", + "GET /geo?ip=208.67.222.222" + ] + }, + "response_format": { + "country": "Country name", + "city": "City name", + "lat": "Latitude", + "lon": "Longitude", + "timezone": "Timezone", + "isp": "Internet Service Provider" + } + }); + + Ok(Response::builder() + .status(200) + .header("Content-Type", "application/json") + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, OPTIONS") + .header("Access-Control-Allow-Headers", "Content-Type") + .body(Body::from(serde_json::to_string(&help_content).unwrap())) + .unwrap()) +} + +async fn handle_geolocation(request: Request) -> Result, Error> { + // Extract IP from query parameter or use a default + let query_params = request.query_string_parameters(); + let ip = query_params.first("ip").unwrap_or("127.0.0.1"); + + // Validate IP format (basic validation) + if ip.parse::().is_err() { + let error_response = ErrorResponse { + error: "Invalid IP".to_string(), + message: format!("'{ip}' is not a valid IP address"), + }; + + return Ok(Response::builder() + .status(400) + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&error_response).unwrap())) + .unwrap()); + } + + // Get geolocation data + match get_geolocation(ip).await { + Ok(geo_data) => { + // Check if the API returned an error + if geo_data.status.as_deref() == Some("fail") { + let error_response = ErrorResponse { + error: "Geolocation lookup failed".to_string(), + message: geo_data.message.unwrap_or_else(|| "Unknown error".to_string()), + }; + + return Ok(Response::builder() + .status(400) + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&error_response).unwrap())) + .unwrap()); + } + + // Return successful response + Ok(Response::builder() + .status(200) + .header("Content-Type", "application/json") + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, OPTIONS") + .header("Access-Control-Allow-Headers", "Content-Type") + .body(Body::from(serde_json::to_string(&geo_data).unwrap())) + .unwrap()) + } + Err(e) => { + let error_response = ErrorResponse { + error: "Internal error".to_string(), + message: format!("Failed to fetch geolocation data: {e}"), + }; + + Ok(Response::builder() + .status(500) + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&error_response).unwrap())) + .unwrap()) + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // Initialize tracing only when dev-tracing feature is enabled + #[cfg(feature = "dev-tracing")] + { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(false) + .without_time() + .init(); + } + + run(service_fn(function_handler)).await +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..289c80f --- /dev/null +++ b/test.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +echo "🧪 Running local tests for lambda_http_geolocation..." + +# 1. Check if code compiles +echo "🔨 Checking if code compiles..." +if ! cargo check; then + echo "❌ Compilation failed!" + exit 1 +fi +echo "✅ Compilation successful" + +# 2. Run clippy checks +echo "🔍 Running clippy checks..." +if ! cargo clippy -- -D warnings; then + echo "❌ Clippy checks failed! Fix warnings before proceeding." + exit 1 +fi +echo "✅ Clippy checks passed" + +# 3. Run tests (if any) +echo "🧪 Running tests..." +if ! cargo test; then + echo "❌ Tests failed!" + exit 1 +fi +echo "✅ Tests passed" + +# 4. Check if function can be built (optional - requires ARM64 target) +echo "🏗️ Checking if function can be built..." +if cargo lambda build --release --arm64 --no-default-features 2>/dev/null; then + echo "✅ Lambda build successful" +else + echo "⚠️ Lambda build skipped (ARM64 target not installed)" + echo " Run './setup.sh' to install ARM64 target for full testing" +fi + +echo "" +echo "🎉 All local tests passed! The function is ready for deployment." +echo "🚀 Next steps:" +echo " ./build.sh # Build and package for deployment" +echo " cd infra && terraform apply # Deploy to AWS"