//! CNI Integration Tests //! //! These tests demonstrate the pod→network attachment flow using the NovaNET CNI plugin. //! //! Test requirements: //! - NovaNET server must be running on localhost:50052 //! - A test VPC and Subnet must be created //! - CNI plugin binary must be built and available //! //! Run with: cargo test --test cni_integration_test -- --ignored use anyhow::Result; use serde_json::json; use std::process::Command; use std::io::Write; use uuid::Uuid; /// Test CNI ADD command with NovaNET backend /// /// This test demonstrates: /// 1. Creating a pod network attachment point /// 2. Allocating an IP address from NovaNET /// 3. Returning network configuration to the container runtime #[tokio::test] #[ignore] // Requires NovaNET server running async fn test_cni_add_creates_novanet_port() -> Result<()> { // Test configuration let container_id = Uuid::new_v4().to_string(); let netns = format!("/var/run/netns/test-{}", container_id); let ifname = "eth0"; // NovaNET test environment let novanet_addr = std::env::var("NOVANET_SERVER_ADDR") .unwrap_or_else(|_| "http://127.0.0.1:50052".to_string()); let subnet_id = std::env::var("TEST_SUBNET_ID") .expect("TEST_SUBNET_ID must be set for integration tests"); let org_id = "test-org"; let project_id = "test-project"; // Build CNI config let cni_config = json!({ "cniVersion": "1.0.0", "name": "k8shost-net", "type": "novanet", "novanet": { "server_addr": novanet_addr, "subnet_id": subnet_id, "org_id": org_id, "project_id": project_id, } }); // Find CNI plugin binary let cni_path = std::env::var("CNI_PLUGIN_PATH") .unwrap_or_else(|_| "./target/debug/novanet-cni".to_string()); println!("Testing CNI ADD with container_id={}", container_id); println!("CNI plugin path: {}", cni_path); // Invoke CNI ADD 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()?; // Write config to stdin if let Some(mut stdin) = child.stdin.take() { stdin.write_all(cni_config.to_string().as_bytes())?; } // Wait for result let output = child.wait_with_output()?; // Check for success if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); panic!("CNI ADD failed: {}", stderr); } // Parse result let result: serde_json::Value = serde_json::from_slice(&output.stdout)?; println!("CNI ADD result: {}", serde_json::to_string_pretty(&result)?); // Verify result structure assert_eq!(result["cniVersion"], "1.0.0"); assert!(result["interfaces"].is_array()); assert!(result["ips"].is_array()); // Extract allocated IP let ip_address = result["ips"][0]["address"] .as_str() .expect("IP address not found in CNI result"); println!("Pod allocated IP: {}", ip_address); // Extract MAC address let mac_address = result["interfaces"][0]["mac"] .as_str() .expect("MAC address not found in CNI result"); println!("Pod MAC address: {}", mac_address); // Verify port was created in NovaNET // (In production, we would query NovaNET to verify the port exists) // Cleanup: Invoke CNI DEL println!("Cleaning up - invoking CNI DEL"); invoke_cni_del(&cni_path, &cni_config, &container_id, &netns, ifname).await?; Ok(()) } /// Test CNI DEL command with NovaNET backend /// /// This test demonstrates: /// 1. Removing a pod network attachment /// 2. Deleting the port from NovaNET #[tokio::test] #[ignore] // Requires NovaNET server running async fn test_cni_del_removes_novanet_port() -> Result<()> { // First create a port using ADD let container_id = Uuid::new_v4().to_string(); let netns = format!("/var/run/netns/test-{}", container_id); let ifname = "eth0"; let novanet_addr = std::env::var("NOVANET_SERVER_ADDR") .unwrap_or_else(|_| "http://127.0.0.1:50052".to_string()); let subnet_id = std::env::var("TEST_SUBNET_ID") .expect("TEST_SUBNET_ID must be set for integration tests"); let cni_config = json!({ "cniVersion": "1.0.0", "name": "k8shost-net", "type": "novanet", "novanet": { "server_addr": novanet_addr, "subnet_id": subnet_id, "org_id": "test-org", "project_id": "test-project", } }); let cni_path = std::env::var("CNI_PLUGIN_PATH") .unwrap_or_else(|_| "./target/debug/novanet-cni".to_string()); // Create port println!("Creating test port with CNI ADD"); invoke_cni_add(&cni_path, &cni_config, &container_id, &netns, ifname).await?; // Now test DEL println!("Testing CNI DEL with container_id={}", container_id); 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()?; if let Some(mut stdin) = child.stdin.take() { stdin.write_all(cni_config.to_string().as_bytes())?; } let output = child.wait_with_output()?; // DEL should succeed if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); println!("CNI DEL stderr: {}", stderr); } assert!(output.status.success(), "CNI DEL should succeed"); println!("CNI DEL succeeded - port removed from NovaNET"); Ok(()) } /// Test complete pod lifecycle: create → network → delete /// /// This test demonstrates the full integration flow: /// 1. Pod is created via k8shost API server /// 2. CNI plugin allocates network port from NovaNET /// 3. Pod receives IP address and MAC address /// 4. Pod is deleted /// 5. CNI plugin removes network port #[tokio::test] #[ignore] // Requires both k8shost and NovaNET servers running async fn test_full_pod_network_lifecycle() -> Result<()> { // This test would: // 1. Create a pod via k8shost API // 2. Simulate kubelet invoking CNI ADD // 3. Update pod status with network info // 4. Delete pod // 5. Simulate kubelet invoking CNI DEL // // For now, this is a placeholder for the full integration test // that would be implemented in S6.2 after all components are wired together println!("Full pod network lifecycle test - placeholder"); println!("This will be implemented after S6.1 completion"); Ok(()) } /// Test multi-tenant network isolation /// /// This test demonstrates: /// 1. Pod from org-a gets network in org-a's subnet /// 2. Pod from org-b gets network in org-b's subnet /// 3. Network isolation is enforced at NovaNET level #[tokio::test] #[ignore] // Requires NovaNET server with multi-tenant setup async fn test_multi_tenant_network_isolation() -> Result<()> { // This test would verify that: // - Org-A pods get IPs from org-a subnets // - Org-B pods get IPs from org-b subnets // - Cross-tenant network access is blocked println!("Multi-tenant network isolation test - placeholder"); println!("This will be implemented in S6.1 after basic flow is validated"); Ok(()) } // Helper functions async fn invoke_cni_add( cni_path: &str, cni_config: &serde_json::Value, container_id: &str, netns: &str, ifname: &str, ) -> Result { 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()?; if let Some(mut stdin) = child.stdin.take() { stdin.write_all(cni_config.to_string().as_bytes())?; } let output = child.wait_with_output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("CNI ADD failed: {}", stderr)); } let result = serde_json::from_slice(&output.stdout)?; Ok(result) } async fn invoke_cni_del( cni_path: &str, cni_config: &serde_json::Value, container_id: &str, netns: &str, ifname: &str, ) -> Result<()> { 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()?; if let Some(mut stdin) = child.stdin.take() { stdin.write_all(cni_config.to_string().as_bytes())?; } let output = child.wait_with_output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); eprintln!("CNI DEL warning: {}", stderr); } Ok(()) }