use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use orca_core::config::{ClusterConfig, ServiceConfig};
use orca_core::runtime::{Runtime, WorkloadHandle};
use orca_core::types::{HealthState, Replicas, WorkloadStatus};
use crate::stats::ContainerStats;
use crate::webhook::WebhookStore;
pub use orca_proxy::{RouteTarget, SharedWasmTriggers, WasmTrigger};
pub type SharedRouteTable = Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>;
pub struct AppState {
pub cluster_config: ClusterConfig,
pub container_runtime: Arc<dyn Runtime>,
pub wasm_runtime: Option<Arc<dyn Runtime>>,
pub services: RwLock<HashMap<String, ServiceState>>,
pub route_table: SharedRouteTable,
pub wasm_triggers: SharedWasmTriggers,
pub registered_nodes: RwLock<HashMap<u64, RegisteredNode>>,
pub webhooks: WebhookStore,
pub api_tokens: Vec<String>,
pub pending_commands: RwLock<HashMap<u64, Vec<serde_json::Value>>>,
pub deploy_history: RwLock<crate::deploy_history::DeployHistory>,
pub acme_manager: Option<orca_proxy::acme::AcmeManager>,
pub cert_resolver: Option<orca_proxy::SharedCertResolver>,
pub container_stats: RwLock<HashMap<String, ContainerStats>>,
pub store: Option<Arc<crate::store::ClusterStore>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RegisteredNode {
pub node_id: u64,
pub address: String,
pub labels: HashMap<String, String>,
pub last_heartbeat: chrono::DateTime<chrono::Utc>,
#[serde(default)]
pub drain: bool,
#[serde(default)]
pub cpu_percent: f64,
#[serde(default)]
pub memory_bytes: u64,
#[serde(default)]
pub memory_total: u64,
#[serde(default)]
pub disk_used: u64,
#[serde(default)]
pub disk_total: u64,
#[serde(default)]
pub net_rx: u64,
#[serde(default)]
pub net_tx: u64,
}
#[derive(Debug)]
pub struct ServiceState {
pub config: ServiceConfig,
pub desired_replicas: u32,
pub instances: Vec<InstanceState>,
}
#[derive(Debug)]
pub struct InstanceState {
pub handle: WorkloadHandle,
pub status: WorkloadStatus,
pub host_port: Option<u16>,
pub container_address: Option<String>,
pub health: HealthState,
pub is_canary: bool,
pub started_at: std::time::Instant,
}
impl AppState {
pub fn new(
cluster_config: ClusterConfig,
container_runtime: Arc<dyn Runtime>,
wasm_runtime: Option<Arc<dyn Runtime>>,
route_table: SharedRouteTable,
wasm_triggers: SharedWasmTriggers,
) -> Self {
let api_tokens = cluster_config.api_tokens.clone();
Self {
cluster_config,
container_runtime,
wasm_runtime,
services: RwLock::new(HashMap::new()),
route_table,
wasm_triggers,
registered_nodes: RwLock::new(HashMap::new()),
pending_commands: RwLock::new(HashMap::new()),
webhooks: crate::webhook::new_store(),
api_tokens,
deploy_history: RwLock::new(crate::deploy_history::DeployHistory::new()),
acme_manager: None,
cert_resolver: None,
container_stats: RwLock::new(HashMap::new()),
store: None,
}
}
pub fn with_store(mut self, store: Arc<crate::store::ClusterStore>) -> Self {
self.store = Some(store);
self
}
pub fn with_acme(
mut self,
manager: orca_proxy::acme::AcmeManager,
resolver: orca_proxy::SharedCertResolver,
) -> Self {
self.acme_manager = Some(manager);
self.cert_resolver = Some(resolver);
self
}
}
impl ServiceState {
pub fn from_config(config: ServiceConfig) -> Self {
let desired_replicas = match &config.replicas {
Replicas::Fixed(n) => *n,
Replicas::Auto => 1,
};
Self {
config,
desired_replicas,
instances: Vec::new(),
}
}
pub fn running_count(&self) -> u32 {
self.instances
.iter()
.filter(|i| i.status == WorkloadStatus::Running)
.count() as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use orca_core::config::ServiceConfig;
use orca_core::runtime::WorkloadHandle;
use orca_core::types::{Replicas, WorkloadStatus};
fn minimal_config(replicas: Replicas) -> ServiceConfig {
ServiceConfig {
name: "test-svc".to_string(),
project: None,
runtime: Default::default(),
image: Some("nginx:latest".to_string()),
module: None,
replicas,
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,
}
}
fn make_instance(status: WorkloadStatus) -> InstanceState {
InstanceState {
handle: WorkloadHandle {
runtime_id: "test-id".to_string(),
name: "test-instance".to_string(),
metadata: HashMap::new(),
},
status,
host_port: None,
container_address: None,
health: HealthState::Unknown,
is_canary: false,
started_at: std::time::Instant::now(),
}
}
#[test]
fn from_config_fixed_sets_desired_replicas() {
let state = ServiceState::from_config(minimal_config(Replicas::Fixed(3)));
assert_eq!(state.desired_replicas, 3);
}
#[test]
fn from_config_auto_defaults_to_one() {
let state = ServiceState::from_config(minimal_config(Replicas::Auto));
assert_eq!(state.desired_replicas, 1);
}
#[test]
fn running_count_with_mixed_statuses() {
let mut state = ServiceState::from_config(minimal_config(Replicas::Fixed(4)));
state.instances = vec![
make_instance(WorkloadStatus::Running),
make_instance(WorkloadStatus::Stopped),
make_instance(WorkloadStatus::Running),
make_instance(WorkloadStatus::Failed),
];
assert_eq!(state.running_count(), 2);
}
}