Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ uuid = { version = "1.9", features = ["serde", "v4"] }
ignore.workspace = true
base64.workspace = true
tokio-util.workspace = true
sha1 = { version = "0.10", optional = true }
hex = { version = "0.4", optional = true }

[workspace.dependencies]
pretty_assertions = "1.4"
Expand Down Expand Up @@ -105,6 +107,7 @@ tempfile = "3.8"
[features]
proxy = ["dep:pingora"]
errorlogs = []
vercel = ["dep:sha1", "dep:hex"]

# The profile that 'dist' will build with
[profile.dist]
Expand Down
16 changes: 16 additions & 0 deletions src/adapters/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,11 @@ pub enum IngressConfig {
account_id: String,
worker_name: String,
},
#[cfg(feature = "vercel")]
Vercel {
project_name: String,
team_id: Option<String>,
},
}

#[derive(Clone, Debug, Serialize)]
Expand All @@ -645,6 +650,12 @@ pub enum MonitorConfig {
account_id: String,
worker_name: String,
},
#[cfg(feature = "vercel")]
Vercel {
api_token: String,
project_name: String,
team_id: Option<String>,
},
}

#[derive(Clone, Debug, Serialize)]
Expand All @@ -658,6 +669,11 @@ pub enum PlatformConfig {
account_id: String,
worker_name: String,
},
#[cfg(feature = "vercel")]
Vercel {
project_name: String,
team_id: Option<String>,
},
}

#[derive(Builder)]
Expand Down
4 changes: 4 additions & 0 deletions src/adapters/ingresses/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub type BoxedIngress = Box<dyn Ingress + Send + Sync>;

pub(crate) use apig::AwsApiGateway;
pub(crate) use cloudflare::CloudflareWorkerIngress;
#[cfg(feature = "vercel")]
pub(crate) use vercel::VercelIngress;

use super::backend::IngressConfig;

Expand Down Expand Up @@ -47,6 +49,8 @@ pub trait Ingress: Shutdownable {

mod apig;
mod cloudflare;
#[cfg(feature = "vercel")]
mod vercel;

#[cfg(test)]
mod tests {
Expand Down
136 changes: 136 additions & 0 deletions src/adapters/ingresses/vercel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#[cfg(feature = "vercel")]
use crate::{
Shutdownable, WholePercent,
adapters::{
backend::IngressConfig,
vercel::VercelClient as Client,
},
subsystems::ShutdownResult,
};

#[cfg(feature = "vercel")]
use super::Ingress;
#[cfg(feature = "vercel")]
use async_trait::async_trait;
#[cfg(feature = "vercel")]
use derive_getters::Getters;
#[cfg(feature = "vercel")]
use miette::Result;
#[cfg(feature = "vercel")]
use tracing::{debug, info};

#[cfg(feature = "vercel")]
#[derive(Getters)]
pub struct VercelIngress {
client: Client,
// The deployment ID of the baseline version
baseline_deployment_id: Option<String>,
// The deployment ID of the canary version
canary_deployment_id: Option<String>,
}

#[cfg(feature = "vercel")]
impl VercelIngress {
pub fn new(client: Client) -> Self {
Self {
client,
baseline_deployment_id: None,
canary_deployment_id: None,
}
}
}

#[cfg(feature = "vercel")]
#[async_trait]
impl Ingress for VercelIngress {
fn get_config(&self) -> IngressConfig {
IngressConfig::Vercel {
project_name: self.client.project_name().clone(),
team_id: self.client.team_id().clone(),
}
}

async fn release_canary(
&mut self,
baseline_deployment_id: String,
canary_deployment_id: String,
) -> Result<()> {
debug!("Releasing canary in Vercel!");

// Save the deployment IDs
self.baseline_deployment_id = Some(baseline_deployment_id.clone());
self.canary_deployment_id = Some(canary_deployment_id.clone());

// Note: Vercel doesn't have built-in canary deployment traffic splitting
// This is a placeholder implementation
// In a real implementation, you would:
// 1. Use Vercel's Edge Config or similar feature for traffic splitting
// 2. Configure a middleware to route traffic based on percentages
// 3. Or use Vercel's deployment promotion API

info!("Canary deployment created: {}", canary_deployment_id);
info!("Baseline deployment: {}", baseline_deployment_id);

Ok(())
}

async fn set_canary_traffic(&mut self, percent: WholePercent) -> Result<()> {
info!("Setting Vercel canary traffic to {percent} (placeholder implementation)");

// Note: Vercel doesn't natively support percentage-based traffic splitting
// like CloudFlare Workers. This would require:
// 1. Setting up Edge Config or Edge Middleware
// 2. Implementing custom traffic routing logic
// 3. Using Vercel's API to update the configuration

// For now, this is a placeholder
debug!(
"Would route {}% traffic to canary: {:?}",
percent.as_i32(),
self.canary_deployment_id
);

Ok(())
}

async fn rollback_canary(&mut self) -> Result<()> {
info!("Rolling back canary in Vercel (placeholder implementation)");

// In a real implementation, this would:
// 1. Remove the canary deployment from production
// 2. Ensure 100% traffic goes to baseline
// 3. Update Edge Config or middleware settings

self.canary_deployment_id = None;

Ok(())
}

async fn promote_canary(&mut self) -> Result<()> {
info!("Promoting canary in Vercel!");

// In a real implementation, this would:
// 1. Make the canary deployment the production deployment
// 2. Update DNS/routing to point to the canary
// 3. Use Vercel's API to promote the deployment

self.canary_deployment_id = None;

Ok(())
}
}

#[cfg(feature = "vercel")]
#[async_trait]
impl Shutdownable for VercelIngress {
async fn shutdown(&mut self) -> ShutdownResult {
// If there's no canary deployment ID set, there's nothing to rollback
if self.canary_deployment_id.is_none() {
debug!("No canary deployment ID set, nothing to rollback.");
return Ok(());
}

self.rollback_canary().await?;
Ok(())
}
}
5 changes: 5 additions & 0 deletions src/adapters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub use backend::BackendClient;
pub(crate) use backend::{LockedState, RolloutMetadata};
pub use cloudflare::CloudflareClient;
#[cfg(feature = "vercel")]
pub use vercel::VercelClient;

pub use ingresses::*;
pub use monitors::*;
Expand All @@ -9,6 +11,9 @@ pub use platforms::*;
pub mod backend;
/// MultiTool's Cloudflare HTTP client.
mod cloudflare;
/// MultiTool's Vercel HTTP client.
#[cfg(feature = "vercel")]
mod vercel;
/// Contains the trait definition and ingress implementations. Ingresses are responsible
/// for actuating changes to traffic.
mod ingresses;
Expand Down
4 changes: 4 additions & 0 deletions src/adapters/monitors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub type StatusCode = CategoricalObservation<5, ResponseStatusCode>;

pub use cloudflare::CloudflareMonitor;
pub use cloudwatch::CloudWatch;
#[cfg(feature = "vercel")]
pub use vercel::VercelMonitor;

use super::backend::MonitorConfig;

Expand Down Expand Up @@ -60,3 +62,5 @@ pub trait Monitor: Shutdownable {

mod cloudflare;
mod cloudwatch;
#[cfg(feature = "vercel")]
mod vercel;
98 changes: 98 additions & 0 deletions src/adapters/monitors/vercel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#[cfg(feature = "vercel")]
use async_trait::async_trait;
#[cfg(feature = "vercel")]
use chrono::{DateTime, Utc};
#[cfg(feature = "vercel")]
use derive_getters::Getters;
#[cfg(feature = "vercel")]
use tracing::info;

#[cfg(feature = "vercel")]
use crate::{
Shutdownable,
adapters::{backend::MonitorConfig, vercel::VercelClient as Client},
metrics::ResponseStatusCode,
stats::{CategoricalObservation, Group},
subsystems::ShutdownResult,
};
#[cfg(feature = "vercel")]
use miette::Result;

#[cfg(feature = "vercel")]
use super::Monitor;

#[cfg(feature = "vercel")]
#[derive(Getters)]
pub struct VercelMonitor {
client: Client,
// The deployment ID of the baseline version
baseline_deployment_id: Option<String>,
// The deployment ID of the canary version
canary_deployment_id: Option<String>,
// The time we started querying
_start_time: DateTime<Utc>,
}

#[cfg(feature = "vercel")]
impl VercelMonitor {
pub fn new(client: Client) -> Self {
Self {
client,
baseline_deployment_id: None,
canary_deployment_id: None,
_start_time: Utc::now(),
}
}
}

#[cfg(feature = "vercel")]
#[async_trait]
impl Monitor for VercelMonitor {
type Item = CategoricalObservation<5, ResponseStatusCode>;

fn get_config(&self) -> MonitorConfig {
MonitorConfig::Vercel {
api_token: self.client.api_token().clone(),
project_name: self.client.project_name().clone(),
team_id: self.client.team_id().clone(),
}
}

async fn query(&mut self) -> Result<Vec<Self::Item>> {
info!("Querying Vercel for metrics (placeholder implementation)");

// Note: Vercel's analytics API requires a paid plan
// This is a placeholder implementation that returns empty metrics
// In a real implementation, you would:
// 1. Query Vercel's analytics API for each deployment
// 2. Parse the response status codes
// 3. Create CategoricalObservations for baseline and canary

let metrics = Vec::new();

// TODO: Implement actual Vercel analytics API integration
// This would require calling Vercel's analytics endpoints
// and parsing the response data

Ok(metrics)
}

async fn set_canary_version_id(&mut self, canary_version_id: String) -> Result<()> {
self.canary_deployment_id = Some(canary_version_id);
Ok(())
}

async fn set_baseline_version_id(&mut self, baseline_version_id: String) -> Result<()> {
self.baseline_deployment_id = Some(baseline_version_id);
Ok(())
}
}

#[cfg(feature = "vercel")]
#[async_trait]
impl Shutdownable for VercelMonitor {
async fn shutdown(&mut self) -> ShutdownResult {
// When we get the shutdown signal, we stop querying
Ok(())
}
}
4 changes: 4 additions & 0 deletions src/adapters/platforms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub type BoxedPlatform = Box<dyn Platform + Send + Sync>;

pub(crate) use cloudflare::CloudflareWorkerPlatform;
pub(crate) use lambda::LambdaPlatform;
#[cfg(feature = "vercel")]
pub(crate) use vercel::VercelPlatform;

use super::backend::PlatformConfig;

Expand Down Expand Up @@ -36,6 +38,8 @@ impl Shutdownable for MockPlatform {

mod cloudflare;
mod lambda;
#[cfg(feature = "vercel")]
mod vercel;

#[cfg(test)]
mod tests {
Expand Down
Loading
Loading