//! CNI Plugin Invocation //! //! This module provides helpers for invoking the NovaNET CNI plugin //! during pod lifecycle operations. //! //! In a production k8s environment, this would be handled by the kubelet //! on each node. For MVP, we provide test helpers to demonstrate the flow. use anyhow::{Context, Result}; use serde_json::json; use std::process::Command; use std::io::Write; /// CNI configuration for pod network setup #[derive(Debug, Clone)] pub struct CniConfig { pub cni_version: String, pub name: String, pub plugin_type: String, pub novanet_server_addr: String, pub subnet_id: String, pub org_id: String, pub project_id: String, } impl Default for CniConfig { fn default() -> Self { Self { cni_version: "1.0.0".to_string(), name: "k8shost-net".to_string(), plugin_type: "novanet".to_string(), novanet_server_addr: "http://127.0.0.1:50052".to_string(), subnet_id: String::new(), org_id: String::new(), project_id: String::new(), } } } /// Invoke CNI ADD command to set up pod networking /// /// This creates a network port in NovaNET and returns the allocated IP/MAC. /// In production, this would be called by the kubelet on the node where the pod runs. pub async fn invoke_cni_add( config: &CniConfig, container_id: &str, netns: &str, ifname: &str, ) -> Result { // Build CNI config JSON let cni_config = json!({ "cniVersion": config.cni_version, "name": config.name, "type": config.plugin_type, "novanet": { "server_addr": config.novanet_server_addr, "subnet_id": config.subnet_id, "org_id": config.org_id, "project_id": config.project_id, } }); // Find CNI plugin binary let cni_path = std::env::var("CNI_PLUGIN_PATH") .unwrap_or_else(|_| "/opt/cni/bin/novanet-cni".to_string()); // Invoke CNI plugin let mut child = Command::new(&cni_path) .env("CNI_COMMAND", "ADD") .env("CNI_CONTAINERID", container_id) .env("CNI_NETNS", netns) .env("CNI_IFNAME", ifname) .env("CNI_PATH", "/opt/cni/bin") .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .context("Failed to spawn CNI plugin")?; // Write config to stdin if let Some(mut stdin) = child.stdin.take() { stdin .write_all(cni_config.to_string().as_bytes()) .context("Failed to write CNI config to stdin")?; } // Wait for result let output = child.wait_with_output().context("Failed to wait for CNI plugin")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("CNI ADD failed: {}", stderr)); } // Parse result let result: CniResult = serde_json::from_slice(&output.stdout) .context("Failed to parse CNI result")?; Ok(result) } /// Invoke CNI DEL command to tear down pod networking /// /// This removes the network port from NovaNET. pub async fn invoke_cni_del( config: &CniConfig, container_id: &str, netns: &str, ifname: &str, ) -> Result<()> { // Build CNI config JSON let cni_config = json!({ "cniVersion": config.cni_version, "name": config.name, "type": config.plugin_type, "novanet": { "server_addr": config.novanet_server_addr, "subnet_id": config.subnet_id, "org_id": config.org_id, "project_id": config.project_id, } }); // Find CNI plugin binary let cni_path = std::env::var("CNI_PLUGIN_PATH") .unwrap_or_else(|_| "/opt/cni/bin/novanet-cni".to_string()); // Invoke CNI plugin let mut child = Command::new(&cni_path) .env("CNI_COMMAND", "DEL") .env("CNI_CONTAINERID", container_id) .env("CNI_NETNS", netns) .env("CNI_IFNAME", ifname) .env("CNI_PATH", "/opt/cni/bin") .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .context("Failed to spawn CNI plugin")?; // Write config to stdin if let Some(mut stdin) = child.stdin.take() { stdin .write_all(cni_config.to_string().as_bytes()) .context("Failed to write CNI config to stdin")?; } // Wait for result let output = child.wait_with_output().context("Failed to wait for CNI plugin")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); tracing::warn!("CNI DEL error (may be expected if already deleted): {}", stderr); } Ok(()) } /// CNI result structure #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct CniResult { #[serde(rename = "cniVersion")] pub cni_version: String, pub interfaces: Vec, pub ips: Vec, pub routes: Vec, pub dns: CniDnsConfig, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct CniInterface { pub name: String, pub mac: String, pub sandbox: String, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct CniIpConfig { pub interface: usize, pub address: String, pub gateway: String, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct CniRoute { pub dst: String, pub gw: String, } #[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] pub struct CniDnsConfig { pub nameservers: Vec, }