orca_control/
deploy_history.rs1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use orca_core::config::ServiceConfig;
10
11const MAX_ENTRIES_PER_SERVICE: usize = 20;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct DeployRecord {
17 pub deploy_id: String,
19 pub service_name: String,
21 pub image: Option<String>,
23 pub config: ServiceConfig,
25 pub timestamp: DateTime<Utc>,
27}
28
29#[derive(Debug, Default)]
31pub struct DeployHistory {
32 entries: HashMap<String, Vec<DeployRecord>>,
33}
34
35impl DeployHistory {
36 pub fn new() -> Self {
38 Self {
39 entries: HashMap::new(),
40 }
41 }
42
43 pub fn record(&mut self, config: &ServiceConfig) {
48 let record = DeployRecord {
49 deploy_id: Uuid::now_v7().to_string(),
50 service_name: config.name.clone(),
51 image: config.image.clone(),
52 config: config.clone(),
53 timestamp: Utc::now(),
54 };
55
56 let history = self.entries.entry(config.name.clone()).or_default();
57 history.push(record);
58
59 if history.len() > MAX_ENTRIES_PER_SERVICE {
61 let excess = history.len() - MAX_ENTRIES_PER_SERVICE;
62 history.drain(..excess);
63 }
64 }
65
66 pub fn get_previous(&self, service_name: &str) -> Option<&DeployRecord> {
70 let history = self.entries.get(service_name)?;
71 if history.len() < 2 {
72 return None;
73 }
74 Some(&history[history.len() - 2])
75 }
76
77 pub fn list(&self, service_name: &str) -> &[DeployRecord] {
79 self.entries
80 .get(service_name)
81 .map(|v| v.as_slice())
82 .unwrap_or(&[])
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use orca_core::types::Replicas;
90 use std::collections::HashMap;
91
92 fn test_config(name: &str, image: &str) -> ServiceConfig {
93 ServiceConfig {
94 name: name.to_string(),
95 project: None,
96 runtime: Default::default(),
97 image: Some(image.to_string()),
98 module: None,
99 replicas: Replicas::Fixed(1),
100 port: Some(8080),
101 domain: None,
102 health: None,
103 readiness: None,
104 liveness: None,
105 env: HashMap::new(),
106 resources: None,
107 volume: None,
108 deploy: None,
109 placement: None,
110 network: None,
111 aliases: vec![],
112 mounts: vec![],
113 routes: vec![],
114 host_port: None,
115 triggers: Vec::new(),
116 assets: None,
117 build: None,
118 tls_cert: None,
119 tls_key: None,
120 internal: false,
121 depends_on: vec![],
122 cmd: vec![],
123 extra_ports: vec![],
124 strip_prefix: None,
125 pull_policy: Default::default(),
126 backup: None,
127 }
128 }
129
130 #[test]
131 fn record_and_list() {
132 let mut history = DeployHistory::new();
133 history.record(&test_config("api", "api:v1"));
134 history.record(&test_config("api", "api:v2"));
135 assert_eq!(history.list("api").len(), 2);
136 assert_eq!(history.list("api")[0].image.as_deref(), Some("api:v1"));
137 }
138
139 #[test]
140 fn get_previous_returns_second_to_last() {
141 let mut history = DeployHistory::new();
142 history.record(&test_config("api", "api:v1"));
143 assert!(history.get_previous("api").is_none());
144 history.record(&test_config("api", "api:v2"));
145 let prev = history.get_previous("api").unwrap();
146 assert_eq!(prev.image.as_deref(), Some("api:v1"));
147 }
148
149 #[test]
150 fn caps_at_max_entries() {
151 let mut history = DeployHistory::new();
152 for i in 0..25 {
153 history.record(&test_config("svc", &format!("svc:v{i}")));
154 }
155 assert_eq!(history.list("svc").len(), MAX_ENTRIES_PER_SERVICE);
156 assert_eq!(history.list("svc")[0].image.as_deref(), Some("svc:v5"));
157 }
158}