use serde::{Deserialize, Serialize};
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
pub struct RolloutFile {
pub rollout: RolloutPolicy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
pub struct RolloutPolicy {
pub strategy: RolloutStrategy,
pub window_seconds: u64,
#[serde(default)]
pub gates: Vec<RolloutGate>,
#[serde(default)]
pub steps: Vec<RolloutStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum RolloutStrategy {
Linear,
CanaryFraction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
pub struct RolloutGate {
pub metric: String,
pub condition: String,
pub window: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
pub struct RolloutStep {
pub mirrors: Vec<String>,
pub gate_window_seconds: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_failure: Option<RolloutOnFailure>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum RolloutOnFailure {
RollbackStep,
RollbackAll,
}
impl RolloutOnFailure {
pub fn for_step(on_failure: Option<&Self>) -> Self {
on_failure.cloned().unwrap_or(Self::RollbackAll)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EXAMPLE_TOML: &str = r#"
[rollout]
strategy = "linear"
window_seconds = 600
[[rollout.gates]]
metric = "http_5xx_rate"
condition = "< 0.01"
window = "5m"
[[rollout.gates]]
metric = "p95_latency_ms"
condition = "< 200"
window = "5m"
[[rollout.steps]]
mirrors = ["yah-marketing-staging"]
gate_window_seconds = 600
[[rollout.steps]]
mirrors = ["yah-marketing-prod"]
gate_window_seconds = 1800
on_failure = "rollback-step"
"#;
#[test]
fn round_trip_toml() {
let file: RolloutFile = toml::from_str(EXAMPLE_TOML).expect("parse toml");
let policy = &file.rollout;
assert_eq!(policy.strategy, RolloutStrategy::Linear);
assert_eq!(policy.window_seconds, 600);
assert_eq!(policy.gates.len(), 2);
assert_eq!(policy.steps.len(), 2);
let step1 = &policy.steps[1];
assert_eq!(step1.mirrors, vec!["yah-marketing-prod".to_string()]);
assert_eq!(step1.gate_window_seconds, 1800);
assert_eq!(step1.on_failure, Some(RolloutOnFailure::RollbackStep));
}
#[test]
fn on_failure_default() {
assert_eq!(RolloutOnFailure::for_step(None), RolloutOnFailure::RollbackAll);
assert_eq!(
RolloutOnFailure::for_step(Some(&RolloutOnFailure::RollbackStep)),
RolloutOnFailure::RollbackStep
);
}
}