use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use orca_core::config::ServiceConfig;
const MAX_ENTRIES_PER_SERVICE: usize = 20;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployRecord {
pub deploy_id: String,
pub service_name: String,
pub image: Option<String>,
pub config: ServiceConfig,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Default)]
pub struct DeployHistory {
entries: HashMap<String, Vec<DeployRecord>>,
}
impl DeployHistory {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn record(&mut self, config: &ServiceConfig) {
let record = DeployRecord {
deploy_id: Uuid::now_v7().to_string(),
service_name: config.name.clone(),
image: config.image.clone(),
config: config.clone(),
timestamp: Utc::now(),
};
let history = self.entries.entry(config.name.clone()).or_default();
history.push(record);
if history.len() > MAX_ENTRIES_PER_SERVICE {
let excess = history.len() - MAX_ENTRIES_PER_SERVICE;
history.drain(..excess);
}
}
pub fn get_previous(&self, service_name: &str) -> Option<&DeployRecord> {
let history = self.entries.get(service_name)?;
if history.len() < 2 {
return None;
}
Some(&history[history.len() - 2])
}
pub fn list(&self, service_name: &str) -> &[DeployRecord] {
self.entries
.get(service_name)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
}
#[cfg(test)]
mod tests {
use super::*;
use orca_core::types::Replicas;
use std::collections::HashMap;
fn test_config(name: &str, image: &str) -> ServiceConfig {
ServiceConfig {
name: name.to_string(),
project: None,
runtime: Default::default(),
image: Some(image.to_string()),
module: None,
replicas: Replicas::Fixed(1),
port: Some(8080),
domain: None,
health: None,
readiness: None,
liveness: None,
env: HashMap::new(),
resources: None,
volume: None,
deploy: None,
placement: None,
network: None,
aliases: vec![],
mounts: vec![],
routes: vec![],
host_port: None,
triggers: Vec::new(),
assets: None,
build: None,
tls_cert: None,
tls_key: None,
internal: false,
depends_on: vec![],
cmd: vec![],
extra_ports: vec![],
strip_prefix: None,
pull_policy: Default::default(),
backup: None,
}
}
#[test]
fn record_and_list() {
let mut history = DeployHistory::new();
history.record(&test_config("api", "api:v1"));
history.record(&test_config("api", "api:v2"));
assert_eq!(history.list("api").len(), 2);
assert_eq!(history.list("api")[0].image.as_deref(), Some("api:v1"));
}
#[test]
fn get_previous_returns_second_to_last() {
let mut history = DeployHistory::new();
history.record(&test_config("api", "api:v1"));
assert!(history.get_previous("api").is_none());
history.record(&test_config("api", "api:v2"));
let prev = history.get_previous("api").unwrap();
assert_eq!(prev.image.as_deref(), Some("api:v1"));
}
#[test]
fn caps_at_max_entries() {
let mut history = DeployHistory::new();
for i in 0..25 {
history.record(&test_config("svc", &format!("svc:v{i}")));
}
assert_eq!(history.list("svc").len(), MAX_ENTRIES_PER_SERVICE);
assert_eq!(history.list("svc")[0].image.as_deref(), Some("svc:v5"));
}
}