diff --git a/Cargo.lock b/Cargo.lock index 61ea54a..fee301b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,7 @@ dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", + "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -196,6 +197,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "aws-sdk-apigateway" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c5691f914793b7ffa9fcfa7f547cb4cc6c1c5e737216be13be26e568f93e97a" +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", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sdk-cloudwatch" version = "1.54.0" @@ -222,6 +245,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-lambda" +version = "1.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8260e5a54f522b1b876dce8bc1857da2af20eb2154025f91253603599fa15006" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sdk-sso" version = "1.49.0" @@ -296,6 +342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1" dependencies = [ "aws-credential-types", + "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", @@ -340,12 +387,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + [[package]] name = "aws-smithy-http" version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" dependencies = [ + "aws-smithy-eventstream", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", @@ -591,7 +650,9 @@ dependencies = [ "async-stream", "async-trait", "aws-config", + "aws-sdk-apigateway", "aws-sdk-cloudwatch", + "aws-sdk-lambda", "bon", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index c6e86d2..e2d25af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,9 @@ path = "src/bin/main.rs" async-stream = "0.3.6" async-trait = "0.1.83" aws-config = { version = "1.1.7", features = ["behavior-version-latest"] } +aws-sdk-apigateway = "1.50.0" aws-sdk-cloudwatch = "1.54.0" +aws-sdk-lambda = "1.56.0" bon = "2.3.0" chrono = "0.4.38" clap = { version = "4.3", features = ["derive"] } diff --git a/src/adapters/ingresses/apig.rs b/src/adapters/ingresses/apig.rs new file mode 100644 index 0000000..396b5ce --- /dev/null +++ b/src/adapters/ingresses/apig.rs @@ -0,0 +1,56 @@ +use std::path::PathBuf; + +use crate::utils::load_default_aws_config; + +use super::Ingress; +use async_trait::async_trait; +use miette::{IntoDiagnostic, Result}; +use tokio::{fs::File, io::AsyncReadExt}; + +use aws_sdk_apigateway::client::Client as GatewayClient; +use aws_sdk_lambda::client::Client as LambdaClient; + +/// AwsApiGateway is the Ingress implementation for AWS API Gateway + Lambda. +/// It's responsible for creating canary deployments on API Gateway, updating their +/// traffic and promoting them, and deploying Lambda functions. +pub struct AwsApiGateway { + /// The Lambda code, loaded from a file as an array of bytes. + /// The AWS SDK handles the encoding from there. + lambda_artifact: Vec, + apig_client: GatewayClient, + lambda_client: LambdaClient, +} + +impl AwsApiGateway { + /// Given a path to the lambda, create a new APIG Ingress. + pub async fn new(artifact_path: PathBuf) -> Result { + let artifact = read_file(artifact_path).await?; + // Now, configure the AWS SDKs. + // TODO: Extract Config into a single location so we don't have to + // repeat this code every time we initialize an AWS client. + let config = load_default_aws_config().await; + let apig_client = GatewayClient::new(config); + let lambda_client = LambdaClient::new(config); + Ok(Self { + lambda_artifact: artifact, + apig_client, + lambda_client, + }) + } +} + +/// given a path to a file, load it as an array of bytes. +async fn read_file(artifact_path: PathBuf) -> Result> { + let mut bytes = Vec::new(); + // Load the lambda from file. + let mut artifact = File::open(artifact_path).await.into_diagnostic()?; + artifact.read_to_end(&mut bytes).await.into_diagnostic()?; + Ok(bytes) +} + +#[async_trait] +impl Ingress for AwsApiGateway { + async fn deploy(&mut self) -> Result<()> { + todo!() + } +} diff --git a/src/adapters/ingresses/mod.rs b/src/adapters/ingresses/mod.rs index 90b5665..ba015d8 100644 --- a/src/adapters/ingresses/mod.rs +++ b/src/adapters/ingresses/mod.rs @@ -34,5 +34,4 @@ impl From for BoxIngress { /// dispatched. pub type BoxIngress = Box; -#[cfg(test)] -mod tests {} +mod apig; diff --git a/src/adapters/monitors/cloudwatch.rs b/src/adapters/monitors/cloudwatch.rs index f516d55..5ad6a2b 100644 --- a/src/adapters/monitors/cloudwatch.rs +++ b/src/adapters/monitors/cloudwatch.rs @@ -1,7 +1,8 @@ use async_trait::async_trait; -use aws_config::BehaviorVersion; -use crate::{metrics::ResponseStatusCode, stats::CategoricalObservation}; +use crate::{ + metrics::ResponseStatusCode, stats::CategoricalObservation, utils::load_default_aws_config, +}; use aws_sdk_cloudwatch::client::Client as AwsClient; use super::Monitor; @@ -12,12 +13,8 @@ pub struct CloudWatch { impl CloudWatch { pub async fn new() -> Self { - // We don't need a particular version, but we should pin to a particular - // behavior so it doens't accidently slip if `latest` gets updated - // without our knowledge. - let behavior = BehaviorVersion::v2024_03_28(); - let config = aws_config::load_defaults(behavior).await; - let client = aws_sdk_cloudwatch::Client::new(&config); + let config = load_default_aws_config().await; + let client = aws_sdk_cloudwatch::Client::new(config); Self { client } } } diff --git a/src/lib.rs b/src/lib.rs index 9d4ee64..74c79f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,3 +15,5 @@ pub mod metrics; mod pipeline; /// Our statistics library. pub mod stats; +/// For utility functions that span multiple modules and use cases. +mod utils; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..6d217af --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,23 @@ +use aws_config::{BehaviorVersion, SdkConfig}; +use tokio::sync::OnceCell; + +/// Load AWS configuration using their standard rules. e.g. AWS_ACCESS_KEY_ID, +/// or session profile information, etc. This function fetches the data only +/// once, the first time it's called, and memoized the results, so all future +/// calls with return the same information. This prevents a race condition where +/// an external process changes an environment variable while this process is running. +pub async fn load_default_aws_config() -> &'static SdkConfig { + AWS_CONFIG_CELL.get_or_init(load_config).await +} + +/// Private, delegate function to be called only within a OnceCell to ensure +/// its locked. When Rust supports async closures, we can move this into a closure +/// to guarantee its only ever called in one place. +async fn load_config() -> SdkConfig { + // We don't need a particular version, but we pin to one to ensure + // it doesn't accidently slip if `latest` gets updated without our knowledge. + let behavior = BehaviorVersion::v2024_03_28(); + aws_config::load_defaults(behavior).await +} + +static AWS_CONFIG_CELL: OnceCell = OnceCell::const_new();