1use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "json-schema")]
12use schemars::JsonSchema;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
19#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
20pub struct RolloutFile {
21 pub rollout: RolloutPolicy,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
27pub struct RolloutPolicy {
28 pub strategy: RolloutStrategy,
30 pub window_seconds: u64,
32 #[serde(default)]
34 pub gates: Vec<RolloutGate>,
35 #[serde(default)]
37 pub steps: Vec<RolloutStep>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
43#[serde(rename_all = "kebab-case")]
44pub enum RolloutStrategy {
45 Linear,
47 CanaryFraction,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
56#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
57pub struct RolloutGate {
58 pub metric: String,
61 pub condition: String,
64 pub window: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
72pub struct RolloutStep {
73 pub mirrors: Vec<String>,
75 pub gate_window_seconds: u64,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub on_failure: Option<RolloutOnFailure>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
86#[serde(rename_all = "kebab-case")]
87pub enum RolloutOnFailure {
88 RollbackStep,
91 RollbackAll,
93}
94
95impl RolloutOnFailure {
96 pub fn for_step(on_failure: Option<&Self>) -> Self {
99 on_failure.cloned().unwrap_or(Self::RollbackAll)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 const EXAMPLE_TOML: &str = r#"
108[rollout]
109strategy = "linear"
110window_seconds = 600
111
112[[rollout.gates]]
113metric = "http_5xx_rate"
114condition = "< 0.01"
115window = "5m"
116
117[[rollout.gates]]
118metric = "p95_latency_ms"
119condition = "< 200"
120window = "5m"
121
122[[rollout.steps]]
123mirrors = ["yah-marketing-staging"]
124gate_window_seconds = 600
125
126[[rollout.steps]]
127mirrors = ["yah-marketing-prod"]
128gate_window_seconds = 1800
129on_failure = "rollback-step"
130"#;
131
132 #[test]
133 fn round_trip_toml() {
134 let file: RolloutFile = toml::from_str(EXAMPLE_TOML).expect("parse toml");
136 let policy = &file.rollout;
137
138 assert_eq!(policy.strategy, RolloutStrategy::Linear);
139 assert_eq!(policy.window_seconds, 600);
140 assert_eq!(policy.gates.len(), 2);
141 assert_eq!(policy.steps.len(), 2);
142
143 let step1 = &policy.steps[1];
144 assert_eq!(step1.mirrors, vec!["yah-marketing-prod".to_string()]);
145 assert_eq!(step1.gate_window_seconds, 1800);
146 assert_eq!(step1.on_failure, Some(RolloutOnFailure::RollbackStep));
147 }
148
149 #[test]
150 fn on_failure_default() {
151 assert_eq!(RolloutOnFailure::for_step(None), RolloutOnFailure::RollbackAll);
152 assert_eq!(
153 RolloutOnFailure::for_step(Some(&RolloutOnFailure::RollbackStep)),
154 RolloutOnFailure::RollbackStep
155 );
156 }
157}