From 6d5d432d80c93f050918cd09c6b6055857cf4f5d Mon Sep 17 00:00:00 2001 From: David Steiner Date: Tue, 9 Jul 2024 12:29:26 +0200 Subject: [PATCH 1/5] Add DynamoDB repository and binary target for Lambda --- Cargo.lock | 341 ++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 + src/bin/serverless.rs | 22 +++ src/repository.rs | 2 + src/repository/dynamodb.rs | 85 +++++++++ 5 files changed, 450 insertions(+), 6 deletions(-) create mode 100644 src/bin/serverless.rs create mode 100644 src/repository/dynamodb.rs diff --git a/Cargo.lock b/Cargo.lock index 02eba1b..920d407 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,37 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-config" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2368fb843e9eec932f7789d64d0e05850f4a79067188c657e572f1f5a7589df0" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes 1.6.0", + "fastrand", + "hex", + "http 0.2.12", + "hyper 0.14.29", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + [[package]] name = "aws-credential-types" version = "1.2.0" @@ -185,6 +216,73 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-sso" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8aee358b755b2738b3ffb8a5b54ee991b28c8a07483a0ff7d49a58305cc2609" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes 1.6.0", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d5ce026f0ae73e06b20be5932150dd0e9b063417fd7c3acf5ca97018b9cbd64" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes 1.6.0", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c820248cb02e4ea83630ad2e43d0721cdbccedba5ac902cd0b6fb84d7271f205" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sigv4" version = "1.2.2" @@ -248,6 +346,16 @@ dependencies = [ "aws-smithy-types", ] +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + [[package]] name = "aws-smithy-runtime" version = "1.6.0" @@ -260,13 +368,17 @@ dependencies = [ "aws-smithy-types", "bytes 1.6.0", "fastrand", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "http-body 1.0.0", "httparse", + "hyper 0.14.29", + "hyper-rustls", "once_cell", "pin-project-lite", "pin-utils", + "rustls", "tokio", "tracing", ] @@ -297,6 +409,7 @@ dependencies = [ "base64-simd", "bytes 1.6.0", "bytes-utils", + "futures-core", "http 0.2.12", "http 1.1.0", "http-body 0.4.6", @@ -309,6 +422,17 @@ dependencies = [ "ryu", "serde", "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +dependencies = [ + "xmlparser", ] [[package]] @@ -488,6 +612,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -790,6 +924,25 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes 1.6.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.5" @@ -947,6 +1100,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +dependencies = [ + "bytes 1.6.0", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.3.1" @@ -956,7 +1133,7 @@ dependencies = [ "bytes 1.6.0", "futures-channel", "futures-util", - "h2", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -968,6 +1145,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.29", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-util" version = "0.1.5" @@ -979,7 +1172,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper", + "hyper 1.3.1", "pin-project-lite", "socket2", "tokio", @@ -1076,7 +1269,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper", + "hyper 1.3.1", "lambda_runtime", "mime", "percent-encoding", @@ -1102,7 +1295,7 @@ dependencies = [ "http-body 1.0.0", "http-body-util", "http-serde", - "hyper", + "hyper 1.3.1", "hyper-util", "lambda_runtime_api_client", "pin-project 1.1.5", @@ -1128,7 +1321,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper", + "hyper 1.3.1", "hyper-util", "tokio", "tower", @@ -1307,6 +1500,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "outref" version = "0.5.1" @@ -1413,7 +1612,7 @@ dependencies = [ "headers", "http 1.1.0", "http-body-util", - "hyper", + "hyper 1.3.1", "hyper-util", "mime", "multer", @@ -1677,6 +1876,21 @@ dependencies = [ "uncased", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1705,18 +1919,103 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.23" @@ -1806,6 +2105,8 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "aws-config", + "aws-sdk-dynamodb", "envy", "poem", "poem-lambda", @@ -2062,6 +2363,16 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.15" @@ -2265,6 +2576,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.2" @@ -2276,6 +2593,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" version = "1.9.1" @@ -2554,6 +2877,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index f3736c1..adf424f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,15 @@ edition = "2021" name = "local" path = "src/bin/local.rs" +[[bin]] +name = "serverless" +path = "src/bin/serverless.rs" + [dependencies] anyhow = "^1.0.86" async-trait = "^0.1" +aws-config = "^1.5" +aws-sdk-dynamodb = "^1.36" envy = "^0.4" poem = "^3.0" poem-lambda = "^5.0" diff --git a/src/bin/serverless.rs b/src/bin/serverless.rs new file mode 100644 index 0000000..591e317 --- /dev/null +++ b/src/bin/serverless.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use std::sync::Arc; + +use serverless_rust_api::api::build_app; +use serverless_rust_api::repository::DynamoDbRepository; +use serverless_rust_api::settings::Settings; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter("info") + .json() + .init(); + let settings = envy::from_env::()?; + let table_name = settings.table_name.expect("table name to be specified"); + + let repository = Arc::new(DynamoDbRepository::new(table_name).await); + let app = build_app(repository)?; + poem_lambda::run(app).await.expect("app to start correctly"); + + Ok(()) +} diff --git a/src/repository.rs b/src/repository.rs index e65afca..d53acf8 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,5 +1,7 @@ mod base; +mod dynamodb; mod memory; pub use base::{Currency, Repository, SharedRepository}; +pub use dynamodb::DynamoDbRepository; pub use memory::InMemoryRepository; diff --git a/src/repository/dynamodb.rs b/src/repository/dynamodb.rs new file mode 100644 index 0000000..3c82117 --- /dev/null +++ b/src/repository/dynamodb.rs @@ -0,0 +1,85 @@ +use aws_config::BehaviorVersion; +use aws_sdk_dynamodb::types::AttributeValue; +use aws_sdk_dynamodb::types::ReturnValue::AllOld; +use aws_sdk_dynamodb::Client; +use serde_dynamo::{from_item, to_item}; +use tracing::error; + +use crate::error::Error; +use crate::repository::{Currency, Repository}; + +pub struct DynamoDbRepository { + client: Client, + table_name: String, +} + +impl DynamoDbRepository { + pub async fn new(table_name: String) -> Self { + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let client = Client::new(&config); + Self { client, table_name } + } +} + +#[async_trait::async_trait] +impl Repository for DynamoDbRepository { + async fn add_currency(&self, currency: Currency) -> crate::error::Result { + let item = to_item(¤cy).unwrap(); + self.client + .put_item() + .table_name(&self.table_name) + .set_item(Some(item)) + .item("pk", AttributeValue::S(currency.code.to_lowercase())) + .send() + .await + .map_err(|err| { + error!(err = debug(err), "failed to create currency"); + Error::Other + })?; + + Ok(currency) + } + + async fn get_currency(&self, code: &str) -> crate::error::Result { + let output = self + .client + .get_item() + .table_name(&self.table_name) + .key("pk", AttributeValue::S(code.to_lowercase())) + .send() + .await + .map_err(|err| { + error!(err = debug(err), "failed to get currency"); + Error::Other + })?; + + if let Some(item) = output.item { + let currency = from_item(item).map_err(|_| Error::Other)?; + Ok(currency) + } else { + Err(Error::NotFound(code.to_string())) + } + } + + async fn delete_currency(&self, code: &str) -> crate::error::Result { + let output = self + .client + .delete_item() + .table_name(&self.table_name) + .key("pk", AttributeValue::S(code.to_lowercase())) + .return_values(AllOld) + .send() + .await + .map_err(|err| { + error!(err = debug(err), "failed to delete currency"); + Error::Other + })?; + + if let Some(item) = output.attributes { + let currency = from_item(item).map_err(|_| Error::Other)?; + Ok(currency) + } else { + Err(Error::NotFound(code.to_string())) + } + } +} From 33dbb967d6fe393388370b76a2c8c6d072623ffa Mon Sep 17 00:00:00 2001 From: David Steiner Date: Tue, 9 Jul 2024 12:37:07 +0200 Subject: [PATCH 2/5] Add settings module --- src/bin/serverless.rs | 3 +-- src/lib.rs | 1 + src/settings.rs | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 src/settings.rs diff --git a/src/bin/serverless.rs b/src/bin/serverless.rs index 591e317..3d62cf1 100644 --- a/src/bin/serverless.rs +++ b/src/bin/serverless.rs @@ -12,9 +12,8 @@ async fn main() -> Result<()> { .json() .init(); let settings = envy::from_env::()?; - let table_name = settings.table_name.expect("table name to be specified"); - let repository = Arc::new(DynamoDbRepository::new(table_name).await); + let repository = Arc::new(DynamoDbRepository::new(settings.table_name).await); let app = build_app(repository)?; poem_lambda::run(app).await.expect("app to start correctly"); diff --git a/src/lib.rs b/src/lib.rs index 98fd066..85168d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,4 @@ pub mod api; pub mod repository; mod error; +pub mod settings; diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..1d3dc8b --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Settings { + #[serde(default)] + pub table_name: String, +} From ab231fe77415afc8a130d34b12d3eeb9354b9ac9 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Tue, 9 Jul 2024 14:44:15 +0200 Subject: [PATCH 3/5] Add CDK stack --- cdk.json | 17 ++ infrastructure/app.py | 9 + infrastructure/stack/__init__.py | 0 infrastructure/stack/api.py | 66 +++++++ infrastructure/stack/stack.py | 85 +++++++++ poetry.lock | 312 +++++++++++++++++++++++++++++++ poetry.toml | 4 + pyproject.toml | 65 +++++++ 8 files changed, 558 insertions(+) create mode 100644 cdk.json create mode 100644 infrastructure/app.py create mode 100644 infrastructure/stack/__init__.py create mode 100644 infrastructure/stack/api.py create mode 100644 infrastructure/stack/stack.py create mode 100644 poetry.lock create mode 100644 poetry.toml create mode 100644 pyproject.toml diff --git a/cdk.json b/cdk.json new file mode 100644 index 0000000..7bacfd8 --- /dev/null +++ b/cdk.json @@ -0,0 +1,17 @@ +{ + "app": "python3 infrastructure/app.py", + "context": { + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, + "@aws-cdk/core:stackRelativeExports": true, + "@aws-cdk/aws-lambda:recognizeVersionProps": true, + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ] + } +} diff --git a/infrastructure/app.py b/infrastructure/app.py new file mode 100644 index 0000000..17ef7f9 --- /dev/null +++ b/infrastructure/app.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import aws_cdk as cdk + +from stack.stack import ServerlessRustStack + +app = cdk.App() +ServerlessRustStack(app, "ServerlessRustStack") + +app.synth() diff --git a/infrastructure/stack/__init__.py b/infrastructure/stack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/stack/api.py b/infrastructure/stack/api.py new file mode 100644 index 0000000..d16b687 --- /dev/null +++ b/infrastructure/stack/api.py @@ -0,0 +1,66 @@ +from aws_cdk.aws_apigatewayv2 import ( + CorsHttpMethod, + CorsPreflightOptions, + HttpApi, + HttpMethod, +) +from aws_cdk.aws_apigatewayv2_authorizers import HttpJwtAuthorizer +from aws_cdk.aws_apigatewayv2_integrations import HttpLambdaIntegration +from aws_cdk.aws_cognito import UserPool, UserPoolClient +from aws_cdk.aws_lambda import IFunction +from constructs import Construct + +ALLOWED_HEADERS = ["Authorization", "Content-Type"] +ALLOWED_METHODS = [ + CorsHttpMethod.GET, + CorsHttpMethod.OPTIONS, + CorsHttpMethod.POST, +] + + +class ServerlessApi(Construct): + def __init__( + self, + scope: Construct, + construct_id: str, + *, + handler: IFunction, + user_pool: UserPool, + user_pool_client: UserPoolClient, + ) -> None: + super().__init__(scope, construct_id) + + self._api = self.build_http_api() + authorizer = self.build_authorizer(user_pool, user_pool_client) + self.setup_lambda_integration(self._api, handler, authorizer) + + @property + def endpoint_url(self) -> str: + return self._api.api_endpoint + + def build_http_api(self) -> HttpApi: + cors_options = CorsPreflightOptions( + allow_headers=ALLOWED_HEADERS, allow_methods=ALLOWED_METHODS + ) + return HttpApi(self, "ServerlessRustApi", cors_preflight=cors_options) + + @staticmethod + def build_authorizer( + user_pool: UserPool, user_pool_client: UserPoolClient + ) -> HttpJwtAuthorizer: + issuer = f"https://cognito-idp.{user_pool.env.region}.amazonaws.com/{user_pool.user_pool_id}" + return HttpJwtAuthorizer( + "JwtAuthorizer", issuer, jwt_audience=[user_pool_client.user_pool_client_id] + ) + + @staticmethod + def setup_lambda_integration( + api: HttpApi, handler: IFunction, authorizer: HttpJwtAuthorizer + ) -> None: + integration = HttpLambdaIntegration("LambdaIntegration", handler) + api.add_routes( + path="/{proxy+}", + methods=[HttpMethod.GET, HttpMethod.POST], + authorizer=authorizer, + integration=integration, + ) diff --git a/infrastructure/stack/stack.py b/infrastructure/stack/stack.py new file mode 100644 index 0000000..1233ffb --- /dev/null +++ b/infrastructure/stack/stack.py @@ -0,0 +1,85 @@ +import pathlib + +from aws_cdk import CfnOutput, Stack +from aws_cdk.aws_lambda import Architecture, Code, Function, Runtime +from aws_cdk.aws_cognito import ( + UserPool, + StandardAttributes, + StandardAttribute, + AuthFlow, + UserPoolClient, +) +from aws_cdk.aws_dynamodb import Table, Attribute, AttributeType +from constructs import Construct + +from stack.api import ServerlessApi + + +class ServerlessRustStack(Stack): + def __init__(self, scope: Construct, construct_id: str) -> None: + super().__init__(scope, construct_id) + + database_table = self.create_database_table() + handler = self.create_function( + "serverless", table_name=database_table.table_name + ) + database_table.grant_read_write_data(handler) + + user_pool, user_pool_client = self.create_user_pool() + api = ServerlessApi( + self, + "ServerlessApi", + handler=handler, + user_pool=user_pool, + user_pool_client=user_pool_client, + ) + + CfnOutput( + self, + "ApiEndpointUrl", + value=api.endpoint_url, + description="The URL of the API endpoint.", + ) + + def create_database_table(self) -> Table: + partition_key = Attribute(name="pk", type=AttributeType.STRING) + return Table( + self, + "DatabaseTable", + table_name="currencies", + partition_key=partition_key, + ) + + def create_function(self, bin_name: str, *, table_name: str) -> Function: + code_path = pathlib.Path.cwd() / "target" / "lambda" / bin_name + code = Code.from_asset(code_path.as_posix()) + + return Function( + self, + "ApiHandler", + code=code, + architecture=Architecture.ARM_64, + runtime=Runtime.PROVIDED_AL2023, + memory_size=256, + handler="does-not-matter", + environment={ + "TABLE_NAME": table_name, + }, + ) + + def create_user_pool(self) -> tuple[UserPool, UserPoolClient]: + standard_attributes = StandardAttributes( + given_name=StandardAttribute(required=True, mutable=True), + family_name=StandardAttribute(required=True, mutable=True), + ) + user_pool = UserPool( + self, + "UserPool", + user_pool_name="rust-demo", + self_sign_up_enabled=False, + standard_attributes=standard_attributes, + ) + auth_flows = AuthFlow(admin_user_password=True, user_srp=True) + user_pool_client = user_pool.add_client("app-client", auth_flows=auth_flows) + + return user_pool, user_pool_client diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..47b7195 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,312 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "aws-cdk-asset-awscli-v1" +version = "2.2.202" +description = "A library that contains the AWS CLI for use in Lambda Layers" +optional = false +python-versions = "~=3.8" +files = [ + {file = "aws-cdk.asset-awscli-v1-2.2.202.tar.gz", hash = "sha256:3ef87d6530736b3a7b0f777fe3b4297994dd40c3ce9306d95f80f48fb18036e8"}, + {file = "aws_cdk.asset_awscli_v1-2.2.202-py3-none-any.whl", hash = "sha256:96205ea2e5e132ec52fabfff37ea25b9b859498f167d05b32564c949822cd331"}, +] + +[package.dependencies] +jsii = ">=1.93.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-asset-kubectl-v20" +version = "2.1.2" +description = "A library that contains kubectl for use in Lambda Layers" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk.asset-kubectl-v20-2.1.2.tar.gz", hash = "sha256:346283e43018a43e3b3ca571de3f44e85d49c038dc20851894cb8f9b2052b164"}, + {file = "aws_cdk.asset_kubectl_v20-2.1.2-py3-none-any.whl", hash = "sha256:7f0617ab6cb942b066bd7174bf3e1f377e57878c3e1cddc21d6b2d13c92d0cc1"}, +] + +[package.dependencies] +jsii = ">=1.70.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-asset-node-proxy-agent-v6" +version = "2.0.3" +description = "@aws-cdk/asset-node-proxy-agent-v6" +optional = false +python-versions = "~=3.8" +files = [ + {file = "aws-cdk.asset-node-proxy-agent-v6-2.0.3.tar.gz", hash = "sha256:b62cb10c69a42cab135e6bc670e3d2d3121fd4f53a0f61e53449da4b12738a6f"}, + {file = "aws_cdk.asset_node_proxy_agent_v6-2.0.3-py3-none-any.whl", hash = "sha256:ef2ff0634ab037e2ebddbe69d7c92515a847c6c8bb2abdfc85b089f5e87761cb"}, +] + +[package.dependencies] +jsii = ">=1.96.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-lib" +version = "2.148.0" +description = "Version 2 of the AWS Cloud Development Kit library" +optional = false +python-versions = "~=3.8" +files = [ + {file = "aws-cdk-lib-2.148.0.tar.gz", hash = "sha256:8d96f514da2c988f20d032d67ce879dca76c0af3828aa7ee38e8b840654ec684"}, + {file = "aws_cdk_lib-2.148.0-py3-none-any.whl", hash = "sha256:af2096aab2e6fbd3c413f3880fbc8d4c04609263500182e6b86966f14accd4b6"}, +] + +[package.dependencies] +"aws-cdk.asset-awscli-v1" = ">=2.2.202,<3.0.0" +"aws-cdk.asset-kubectl-v20" = ">=2.1.2,<3.0.0" +"aws-cdk.asset-node-proxy-agent-v6" = ">=2.0.3,<3.0.0" +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.101.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "cattrs" +version = "23.2.3" +description = "Composable complex class support for attrs and dataclasses." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, +] + +[package.dependencies] +attrs = ">=23.1.0" + +[package.extras] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] + +[[package]] +name = "constructs" +version = "10.3.0" +description = "A programming model for software-defined state" +optional = false +python-versions = "~=3.7" +files = [ + {file = "constructs-10.3.0-py3-none-any.whl", hash = "sha256:2972f514837565ff5b09171cfba50c0159dfa75ee86a42921ea8c86f2941b3d2"}, + {file = "constructs-10.3.0.tar.gz", hash = "sha256:518551135ec236f9cc6b86500f4fbbe83b803ccdc6c2cb7684e0b7c4d234e7b1"}, +] + +[package.dependencies] +jsii = ">=1.90.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "importlib-resources" +version = "6.4.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] + +[[package]] +name = "jsii" +version = "1.101.0" +description = "Python client for jsii runtime" +optional = false +python-versions = "~=3.8" +files = [ + {file = "jsii-1.101.0-py3-none-any.whl", hash = "sha256:b78b87f8316560040ad0b9dca1682d73b6532a33acf4ecf56185d1ae5edb54fa"}, + {file = "jsii-1.101.0.tar.gz", hash = "sha256:043c4d3d0d09af3c7265747f4da9c95770232477f75c846640df4c63d01b19cb"}, +] + +[package.dependencies] +attrs = ">=21.2,<24.0" +cattrs = ">=1.8,<23.3" +importlib-resources = ">=5.2.0" +publication = ">=0.0.3" +python-dateutil = "*" +typeguard = ">=2.13.3,<2.14.0" +typing-extensions = ">=3.8,<5.0" + +[[package]] +name = "mypy" +version = "1.10.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "publication" +version = "0.0.3" +description = "Publication helps you maintain public-api-friendly modules by preventing unintentional access to private implementation details via introspection." +optional = false +python-versions = "*" +files = [ + {file = "publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6"}, + {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "ruff" +version = "0.3.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, + {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, + {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, + {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, + {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "typeguard" +version = "2.13.3" +description = "Run-time type checker for Python" +optional = false +python-versions = ">=3.5.3" +files = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] + +[package.extras] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy", "pytest", "typing-extensions"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "5d512df7e787bea4020992f176ea4e0078d954dff48be80214d683c2c87a4c24" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..edfc933 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,4 @@ +[virtualenvs] +in-project = true +create = true +path = ".venv" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0407ac1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[tool.poetry] +name = "serverless-api" +version = "0.0.0" +description = "Infrastructure for a serverless Rust API." +authors = [] +readme = "README.md" +packages = [{ include = "stack", from = "infrastructure" }] + +[tool.poetry.group.main.dependencies] +python = "^3.11" + +aws-cdk-lib = "^2.147" + +[tool.poetry.group.dev.dependencies] +mypy = "^1.10" +ruff = "^0.3" + +[tool.mypy] +ignore_missing_imports = true +disallow_untyped_defs = true + +[tool.ruff] +exclude = [ + ".git", + ".git-rewrite", + ".ipynb_checkpoints", + ".mypy_cache", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "build", + "dist", + "site-packages", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "C", # flake8-comprehensions + "B", # flake8-bugbear + "N", # PEP 8 naming convention +] + +ignore = [ + "E501", # line too long, handled by formatter + "W291", # trailing whitespace, handled by formatter +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From eac50e402811695463364577a2958a429637a100 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Wed, 10 Jul 2024 09:57:12 +0200 Subject: [PATCH 4/5] Add utility scripts for Cognito --- utilities/create-user.sh | 15 +++++++++++++++ utilities/get-token.sh | 11 +++++++++++ 2 files changed, 26 insertions(+) create mode 100755 utilities/create-user.sh create mode 100755 utilities/get-token.sh diff --git a/utilities/create-user.sh b/utilities/create-user.sh new file mode 100755 index 0000000..2df6cf2 --- /dev/null +++ b/utilities/create-user.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +USER_ID=$1 +PASSWORD=$2 +EMAIL=$3 +FIRST_NAME=$4 +LAST_NAME=$5 + +USER_POOL_ID=$(aws cognito-idp list-user-pools --max-results 5 --output json | jq -r '.UserPools[] | select(.Name == "rust-demo") | .Id') + +aws cognito-idp admin-create-user --user-pool-id "$USER_POOL_ID" --username "$USER_ID" --user-attributes Name=email,Value="$EMAIL" Name=given_name,Value="$FIRST_NAME" Name=family_name,Value="$LAST_NAME" +aws cognito-idp admin-set-user-password --user-pool-id "$USER_POOL_ID" --username "$USER_ID" --password "$PASSWORD" --permanent +aws cognito-idp admin-update-user-attributes --user-pool-id "$USER_POOL_ID" --username "$USER_ID" --user-attributes Name=email_verified,Value=true \ No newline at end of file diff --git a/utilities/get-token.sh b/utilities/get-token.sh new file mode 100755 index 0000000..f4b8da7 --- /dev/null +++ b/utilities/get-token.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +USER_ID=$1 +PASSWORD=$2 + +USER_POOL_ID=$(aws cognito-idp list-user-pools --max-results 5 --output json | jq -r '.UserPools[] | select(.Name == "rust-demo") | .Id') +CLIENT_ID=$(aws cognito-idp list-user-pool-clients --user-pool-id "$USER_POOL_ID" --max-results 1 | jq -r '.UserPoolClients[0].ClientId') + +aws cognito-idp admin-initiate-auth --user-pool-id "$USER_POOL_ID" --client-id "$CLIENT_ID" --auth-flow ADMIN_USER_PASSWORD_AUTH --auth-parameters USERNAME="$USER_ID",PASSWORD="$PASSWORD" | jq -r '.AuthenticationResult.IdToken' From 9148e1f632a147fdcddaf5ddf2ac27b5eeba8b89 Mon Sep 17 00:00:00 2001 From: David Steiner Date: Wed, 10 Jul 2024 09:59:46 +0200 Subject: [PATCH 5/5] Add Python quality checks to CI workflow --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ infrastructure/app.py | 1 - infrastructure/stack/stack.py | 10 +++++----- pyproject.toml | 7 ++++--- src/settings.rs | 1 - 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d7eae7..84e990f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,27 @@ on: branches: [ "main" ] jobs: + python-quality-checks: + name: Python quality checks + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install poetry + run: pipx install poetry==1.8.2 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: poetry install + - name: Lint + run: poetry run ruff check . + - name: Check formatting + run: poetry run ruff format --check . + - name: Type checking + run: poetry run mypy infrastructure rust-quality-checks: name: Rust quality checks runs-on: ubuntu-latest diff --git a/infrastructure/app.py b/infrastructure/app.py index 17ef7f9..542129d 100644 --- a/infrastructure/app.py +++ b/infrastructure/app.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import aws_cdk as cdk - from stack.stack import ServerlessRustStack app = cdk.App() diff --git a/infrastructure/stack/stack.py b/infrastructure/stack/stack.py index 1233ffb..e4814ec 100644 --- a/infrastructure/stack/stack.py +++ b/infrastructure/stack/stack.py @@ -1,15 +1,15 @@ import pathlib from aws_cdk import CfnOutput, Stack -from aws_cdk.aws_lambda import Architecture, Code, Function, Runtime from aws_cdk.aws_cognito import ( - UserPool, - StandardAttributes, - StandardAttribute, AuthFlow, + StandardAttribute, + StandardAttributes, + UserPool, UserPoolClient, ) -from aws_cdk.aws_dynamodb import Table, Attribute, AttributeType +from aws_cdk.aws_dynamodb import Attribute, AttributeType, Table +from aws_cdk.aws_lambda import Architecture, Code, Function, Runtime from constructs import Construct from stack.api import ServerlessApi diff --git a/pyproject.toml b/pyproject.toml index 0407ac1..d48d58b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,12 +40,13 @@ exclude = [ [tool.ruff.lint] select = [ + "B", # flake8-bugbear + "C", # flake8-comprehensions "E", # pycodestyle errors - "W", # pycodestyle warnings "F", # pyflakes - "C", # flake8-comprehensions - "B", # flake8-bugbear + "I", # imports "N", # PEP 8 naming convention + "W", # pycodestyle warnings ] ignore = [ diff --git a/src/settings.rs b/src/settings.rs index 1d3dc8b..03274c0 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,6 +2,5 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Settings { - #[serde(default)] pub table_name: String, }