kumobad 0.1.0

HTTP daemon and per-VM worker serving kumoba microVM sandboxes
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum NetworkMode {
    #[default]
    Open,
    Allowlist,
    Blocked,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NetworkSpec {
    #[serde(default)]
    pub mode: NetworkMode,
    #[serde(default)]
    pub hosts: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateRequest {
    pub image: String,
    #[serde(default)]
    pub labels: BTreeMap<String, String>,
    #[serde(default = "default_cpu")]
    pub cpu: u8,
    #[serde(default = "default_memory")]
    pub memory_mib: u32,
    #[serde(default)]
    pub auto_stop_minutes: u32,
    #[serde(default)]
    pub auto_delete_minutes: Option<u32>,
    #[serde(default)]
    pub network: NetworkSpec,
}

fn default_cpu() -> u8 {
    2
}

fn default_memory() -> u32 {
    4096
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UpdateRequest {
    #[serde(default)]
    pub labels: Option<BTreeMap<String, String>>,
    #[serde(default)]
    pub network: Option<NetworkSpec>,
    #[serde(default)]
    pub auto_stop_minutes: Option<u32>,
    #[serde(default)]
    pub auto_delete_minutes: Option<Option<u32>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Meta {
    pub id: String,
    pub image: String,
    pub labels: BTreeMap<String, String>,
    pub cpu: u8,
    pub memory_mib: u32,
    pub auto_stop_minutes: u32,
    pub auto_delete_minutes: Option<u32>,
    pub network: NetworkSpec,
    pub created_at_ms: u64,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Summary {
    #[serde(flatten)]
    pub meta: Meta,
    pub state: &'static str,
    pub last_activity_at_ms: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExecRequest {
    pub argv: Vec<String>,
    #[serde(default)]
    pub cwd: Option<String>,
    #[serde(default)]
    pub env: BTreeMap<String, String>,
    #[serde(default = "default_timeout")]
    pub timeout_ms: u64,
    #[serde(default)]
    pub user: Option<String>,
}

fn default_timeout() -> u64 {
    600_000
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SecretSpec {
    pub name: String,
    pub value: String,
    pub hosts: Vec<String>,
    pub env_vars: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SecretsRequest {
    pub secrets: Vec<SecretSpec>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeleteSecretsRequest {
    pub names: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PreviewRequest {
    pub port: u16,
    pub ttl_seconds: u32,
}

pub fn labels_match(labels: &BTreeMap<String, String>, filter: &[(String, String)]) -> bool {
    filter
        .iter()
        .all(|(key, value)| labels.get(key).is_some_and(|actual| actual == value))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn label_filter_requires_every_pair() {
        let labels = BTreeMap::from([("org".into(), "a".into()), ("session".into(), "s".into())]);
        assert!(labels_match(&labels, &[("org".into(), "a".into())]));
        assert!(!labels_match(
            &labels,
            &[("org".into(), "a".into()), ("session".into(), "x".into())]
        ));
        assert!(labels_match(&labels, &[]));
    }

    #[test]
    fn create_defaults_to_open_network_and_no_auto_delete() {
        let request: CreateRequest = serde_json::from_str(r#"{"image":"default"}"#).unwrap();
        assert_eq!(request.network.mode, NetworkMode::Open);
        assert_eq!(request.auto_delete_minutes, None);
        assert_eq!(request.cpu, 2);
    }
}