use super::types::{
Abort, Delay, Destination, HttpFaultInjection, HttpMatchRequest, HttpRoute,
HttpRouteDestination, Percent, PortSelector, StringMatch, VirtualService, VirtualServiceSpec,
};
use kube::Api;
use tracing::info;
pub struct TrafficManager {
client: kube::Client,
namespace: String,
}
impl TrafficManager {
pub fn new(client: kube::Client, namespace: String) -> Self {
Self { client, namespace }
}
pub async fn create_canary_routing(
&self,
service: &str,
stable_version: &str,
canary_version: &str,
canary_percent: u32,
) -> Result<(), Error> {
info!(
"Creating canary routing for service: {} (canary: {}%)",
service, canary_percent
);
let virtual_services: Api<VirtualService> =
Api::namespaced(self.client.clone(), &self.namespace);
let routes: _ = vec![
HttpRoute {
r#match: Some(vec![HttpMatchRequest {
uri: None,
headers: Some(HashMap::from([(
"x-canary".to_string(),
StringMatch {
exact: Some("true".to_string()),
prefix: None,
regex: None,
},
)])),
}]),
route: Some(vec![HttpRouteDestination {
destination: Destination {
host: service.to_string(),
subset: Some(canary_version.to_string()),
port: Some(PortSelector { number: Some(8080) }),
},
weight: Some(canary_percent as i32),
}]),
fault: None,
timeout: None,
retries: None,
},
HttpRoute {
r#match: None,
route: Some(vec![HttpRouteDestination {
destination: Destination {
host: service.to_string(),
subset: Some(stable_version.to_string()),
port: Some(PortSelector { number: Some(8080) }),
},
weight: Some((100 - canary_percent) as i32),
}]),
fault: None,
timeout: None,
retries: None,
},
];
let vs_spec: _ = VirtualServiceSpec {
hosts: vec![service.to_string()],
gateways: Some(vec![format!("{}-gateway", service)]),
http: Some(routes),
};
let vs: _ = VirtualService::new(&format!("{}-canary", service), vs_spec);
let params: _ = kube::api::PostParams::default();
virtual_services.create(¶ms, &vs).await?;
info!("Created canary routing for service: {}", service);
Ok(())
}
pub async fn create_ab_test_routing(
&self,
service: &str,
version_a: &str,
version_b: &str,
split_percent: u32,
) -> Result<(), Error> {
info!(
"Creating A/B testing routing for service: {} (split: {}%)",
service, split_percent
);
let virtual_services: Api<VirtualService> =
Api::namespaced(self.client.clone(), &self.namespace);
let routes: _ = vec![
HttpRoute {
r#match: Some(vec![HttpMatchRequest {
uri: None,
headers: Some(HashMap::from([(
"x-experiment".to_string(),
StringMatch {
exact: Some("version-a".to_string()),
prefix: None,
regex: None,
},
)])),
}]),
route: Some(vec![HttpRouteDestination {
destination: Destination {
host: service.to_string(),
subset: Some(version_a.to_string()),
port: Some(PortSelector { number: Some(8080) }),
},
weight: Some(split_percent as i32),
}]),
fault: None,
timeout: None,
retries: None,
},
HttpRoute {
r#match: None,
route: Some(vec![HttpRouteDestination {
destination: Destination {
host: service.to_string(),
subset: Some(version_b.to_string()),
port: Some(PortSelector { number: Some(8080) }),
},
weight: Some((100 - split_percent) as i32),
}]),
fault: None,
timeout: None,
retries: None,
},
];
let vs_spec: _ = VirtualServiceSpec {
hosts: vec![service.to_string()],
gateways: Some(vec![format!("{}-gateway", service)]),
http: Some(routes),
};
let vs: _ = VirtualService::new(&format!("{}-ab-test", service), vs_spec);
let params: _ = kube::api::PostParams::default();
virtual_services.create(¶ms, &vs).await?;
info!("Created A/B testing routing for service: {}", service);
Ok(())
}
pub async fn apply_fault_injection(
&self,
service: &str,
fault_type: FaultType,
percentage: u32,
) -> Result<(), Error> {
info!(
"Applying fault injection for service: {} (type: {:?}, percentage: {}%)",
service, fault_type, percentage
);
let virtual_services: Api<VirtualService> =
Api::namespaced(self.client.clone(), &self.namespace);
let vs: _ = virtual_services.get(service).await?;
let updated_http: _ = vs.spec.http.map(|routes| {
routes
.into_iter()
.map(|mut route| {
route.fault = Some(HttpFaultInjection {
delay: match fault_type {
FaultType::Delay => Some(Delay {
fixed_delay: Some("5s".to_string()),
percentage: Some(Percent {
value: percentage as f64,
}),
}),
FaultType::Abort => None,
},
abort: match fault_type {
FaultType::Abort => Some(Abort {
http_status: Some(500),
percentage: Some(Percent {
value: percentage as f64,
}),
}),
FaultType::Delay => None,
},
});
route
})
.collect::<Vec<HttpRoute>>()
});
let params: _ = kube::api::PatchParams::default();
let patch: _ = serde_json::json!({
"spec": {
"http": updated_http
}
});
virtual_services
.patch(service, ¶ms, &kube::api::Patch::Merge(&patch))
.await?;
info!("Applied fault injection for service: {}", service);
Ok(())
}
pub async fn remove_fault_injection(&self, service: &str) -> Result<(), Error> {
info!("Removing fault injection for service: {}", service);
let virtual_services: Api<VirtualService> =
Api::namespaced(self.client.clone(), &self.namespace);
let vs: _ = virtual_services.get(service).await?;
let updated_http: _ = vs.spec.http.map(|routes| {
routes
.into_iter()
.map(|mut route| {
route.fault = None;
route
})
.collect::<Vec<_>>()
});
let params: _ = kube::api::PatchParams::default();
let patch: _ = serde_json::json!({
"spec": {
"http": updated_http
}
});
virtual_services
.patch(service, ¶ms, &kube::api::Patch::Merge(&patch))
.await?;
info!("Removed fault injection for service: {}", service);
Ok(())
}
pub async fn create_traffic_mirror(
&self,
service: &str,
_mirror_service: &str,
) -> Result<(), Error> {
info!(
"Creating traffic mirror: {} -> {}",
service, _mirror_service
);
let virtual_services: Api<VirtualService> =
Api::namespaced(self.client.clone(), &self.namespace);
let _vs: _ = virtual_services.get(service).await?;
info!("Traffic mirror setup for service: {}", service);
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum FaultType {
Delay,
Abort,
}
#[derive(Debug, Clone)]
pub struct TrafficSplit {
pub service: String,
pub splits: Vec<(String, u32)>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Kubernetes error: {0}")]
Kube(#[from] kube::Error),
#[error("Serde error: {0}")]
Serde(#[from] serde_json::Error),
#[error("Other error: {0}")]
Other(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traffic_split_creation() {
let split: _ = TrafficSplit {
service: "amberjs-api".to_string(),
splits: vec![("v1".to_string(), 90), ("v2".to_string(), 10)],
};
assert_eq!(split.service, "amberjs-api");
assert_eq!(split.splits.len(), 2);
assert_eq!(split.splits[0].0, "v1");
assert_eq!(split.splits[0].1, 90);
assert_eq!(split.splits[1].0, "v2");
assert_eq!(split.splits[1].1, 10);
}
#[test]
fn test_fault_type() {
let delay: _ = FaultType::Delay;
let abort: _ = FaultType::Abort;
assert!(matches!(delay, FaultType::Delay));
assert!(matches!(abort, FaultType::Abort));
}
}
use std::collections::{BTreeMap, HashMap};