From 0dbd4dd05cd397a8494e90fdb559bacd919b7aaa Mon Sep 17 00:00:00 2001 From: Eric Ghildyal Date: Thu, 21 Nov 2024 16:06:48 -0500 Subject: [PATCH 1/3] Add canary traffic changing --- src/adapters/ingresses/apig.rs | 46 +++++++++++++++++++++++++++++++--- src/adapters/ingresses/mod.rs | 7 +++++- src/lib.rs | 1 + src/pipeline/mod.rs | 2 ++ src/pipeline/percent.rs | 4 +-- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/adapters/ingresses/apig.rs b/src/adapters/ingresses/apig.rs index a98d686..164f4fa 100644 --- a/src/adapters/ingresses/apig.rs +++ b/src/adapters/ingresses/apig.rs @@ -1,9 +1,11 @@ use std::path::PathBuf; use crate::utils::load_default_aws_config; +use crate::WholePercent; use super::Ingress; use async_trait::async_trait; +use aws_sdk_apigateway::types::{Op, PatchOperation}; use miette::miette; use miette::{IntoDiagnostic, Result}; use tokio::{fs::File, io::AsyncReadExt}; @@ -84,7 +86,7 @@ impl AwsApiGateway { stage_name: &str, lambda_name: &str, lambda_version: &str, - traffic_percentage: f64, + traffic_percentage: WholePercent, ) -> Result<()> { // Update the APIG with the new lambda version self.apig_client @@ -105,8 +107,7 @@ impl AwsApiGateway { .stage_name(stage_name) .canary_settings( DeploymentCanarySettings::builder() - .percent_traffic(traffic_percentage) - .use_stage_cache(false) + .percent_traffic(traffic_percentage.into_inner() as f64) .build(), ) .send() @@ -115,6 +116,30 @@ impl AwsApiGateway { Ok(()) } + + pub async fn update_canary_traffic( + &self, + api_id: &str, + stage_name: &str, + traffic_percentage: WholePercent, + ) -> Result<()> { + let patch_op = PatchOperation::builder() + .op(Op::Replace) + .path("/canarySettings/percentTraffic") + .value(traffic_percentage.to_string()) + .build(); + + self.apig_client + .update_stage() + .rest_api_id(api_id) + .stage_name(stage_name) + .patch_operations(patch_op) + .send() + .await + .into_diagnostic()?; + + Ok(()) + } } /// given a path to a file, load it as an array of bytes. @@ -134,7 +159,20 @@ impl Ingress for AwsApiGateway { // Next, we need to create a new deployment, pointing at our // new lambda version with canary settings - self.create_apig_deployment("Releases", "prod", "releases", &lambda_version, 0.0) + self.create_apig_deployment( + "Releases", + "prod", + "releases", + &lambda_version, + WholePercent::try_new(0).into_diagnostic()?, + ) + .await?; + + Ok(()) + } + + async fn set_canary_traffic(&mut self, percent: WholePercent) -> Result<()> { + self.update_canary_traffic("Releases", "prod", percent) .await?; Ok(()) diff --git a/src/adapters/ingresses/mod.rs b/src/adapters/ingresses/mod.rs index c855e0f..653bd93 100644 --- a/src/adapters/ingresses/mod.rs +++ b/src/adapters/ingresses/mod.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use miette::Result; +use crate::WholePercent; pub use apig::AwsApiGateway; /// Ingresses are responsible for (1) controlling how much traffic the canary @@ -14,7 +15,7 @@ pub trait Ingress { // TODO: define the other methods on this type. // async fn yank_canary(&mut self) -> Result<()>; // async fn promote_canary(&mut self) -> Result<()>; - // async fn set_canary_traffic(&mut self, percent: u8); + async fn set_canary_traffic(&mut self, percent: WholePercent) -> Result<()>; } pub struct MockIngress; @@ -24,6 +25,10 @@ impl Ingress for MockIngress { async fn deploy(&mut self) -> Result<()> { todo!() } + + async fn set_canary_traffic(&mut self, _percent: WholePercent) -> Result<()> { + todo!() + } } impl From for BoxIngress { diff --git a/src/lib.rs b/src/lib.rs index 74c79f8..5e6ccde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub use config::Flags; pub use pipeline::Pipeline; +pub(crate) use pipeline::WholePercent; /// An adapter connects to some observable resource (like `CloudWatch`) and /// emits events, like failed and succeeded requests. diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index fd4995c..b263c3d 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -7,6 +7,8 @@ use crate::{ use bon::bon; use miette::Result; +pub(crate) use percent::WholePercent; + /// An alias for the Response Code-based monitor. pub type ResponseMonitor = Box>>; diff --git a/src/pipeline/percent.rs b/src/pipeline/percent.rs index c652d5a..109a631 100644 --- a/src/pipeline/percent.rs +++ b/src/pipeline/percent.rs @@ -22,11 +22,11 @@ impl DecimalPercent { validate(less_or_equal = 100), derive(Debug, Display, Copy, Clone, PartialEq, Eq, TryFrom, Into) )] -pub(super) struct WholePercent(u8); +pub(crate) struct WholePercent(u8); impl WholePercent { /// returns "the rest" of the whole. That is, `100 - this value`. - pub(super) fn inverse(self) -> Self { + pub(crate) fn inverse(self) -> Self { let val = u8::from(self); Self::try_from(100 - val).unwrap() } From 823cb8b485e0ddfacad3b8173afc6fe11b6753f3 Mon Sep 17 00:00:00 2001 From: Eric Ghildyal Date: Mon, 25 Nov 2024 15:14:26 -0500 Subject: [PATCH 2/3] Add helper method for apig name -> apig id --- .gitignore | 1 + src/adapters/ingresses/apig.rs | 29 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..99b1fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +*.zip \ No newline at end of file diff --git a/src/adapters/ingresses/apig.rs b/src/adapters/ingresses/apig.rs index 164f4fa..db2bbcc 100644 --- a/src/adapters/ingresses/apig.rs +++ b/src/adapters/ingresses/apig.rs @@ -5,7 +5,7 @@ use crate::WholePercent; use super::Ingress; use async_trait::async_trait; -use aws_sdk_apigateway::types::{Op, PatchOperation}; +use aws_sdk_apigateway::types::{Op, PatchOperation, RestApi}; use miette::miette; use miette::{IntoDiagnostic, Result}; use tokio::{fs::File, io::AsyncReadExt}; @@ -117,12 +117,35 @@ impl AwsApiGateway { Ok(()) } + pub async fn get_api_id_by_name(&self, api_name: &str) -> Result { + // Given an API Gateway's name, return its auto-generated AWS ID + let all_apis = self + .apig_client + .get_rest_apis() + .send() + .await + .into_diagnostic()?; + + let api = all_apis + .items() + .iter() + .find(|api| api.name.clone().unwrap() == api_name) + .ok_or(miette!( + "Could not find an API Gateway with the name: {}", + api_name + ))?; + + Ok(api.clone()) + } + pub async fn update_canary_traffic( &self, - api_id: &str, + api_name: &str, stage_name: &str, traffic_percentage: WholePercent, ) -> Result<()> { + let api = self.get_api_id_by_name(api_name).await?; + let patch_op = PatchOperation::builder() .op(Op::Replace) .path("/canarySettings/percentTraffic") @@ -131,7 +154,7 @@ impl AwsApiGateway { self.apig_client .update_stage() - .rest_api_id(api_id) + .rest_api_id(api.id.unwrap_or_default()) .stage_name(stage_name) .patch_operations(patch_op) .send() From 341dd290c5cd0db308daa74d3871b5463baf4d86 Mon Sep 17 00:00:00 2001 From: Eric Ghildyal Date: Mon, 25 Nov 2024 15:38:02 -0500 Subject: [PATCH 3/3] Update plumbing to use values from the ApiGateway struct --- src/adapters/ingresses/apig.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/adapters/ingresses/apig.rs b/src/adapters/ingresses/apig.rs index db2bbcc..754289c 100644 --- a/src/adapters/ingresses/apig.rs +++ b/src/adapters/ingresses/apig.rs @@ -178,14 +178,14 @@ async fn read_file(artifact_path: PathBuf) -> Result> { impl Ingress for AwsApiGateway { async fn deploy(&mut self) -> Result<()> { // First, we need to deploy the new version of the lambda - let lambda_version = self.upload_lambda("releases").await?; + let lambda_version = self.upload_lambda(&self.lambda_name).await?; // Next, we need to create a new deployment, pointing at our // new lambda version with canary settings self.create_apig_deployment( - "Releases", - "prod", - "releases", + &self.gateway_name, + &self.stage_name, + &self.lambda_name, &lambda_version, WholePercent::try_new(0).into_diagnostic()?, ) @@ -195,7 +195,7 @@ impl Ingress for AwsApiGateway { } async fn set_canary_traffic(&mut self, percent: WholePercent) -> Result<()> { - self.update_canary_traffic("Releases", "prod", percent) + self.update_canary_traffic(&self.gateway_name, &self.stage_name, percent) .await?; Ok(())