photoncloud-monorepo/k8shost/crates/k8shost-server/src/cni.rs
centra a7ec7e2158 Add T026 practical test + k8shost to flake + workspace files
- Created T026-practical-test task.yaml for MVP smoke testing
- Added k8shost-server to flake.nix (packages, apps, overlays)
- Staged all workspace directories for nix flake build
- Updated flake.nix shellHook to include k8shost

Resolves: T026.S1 blocker (R8 - nix submodule visibility)
2025-12-09 06:07:50 +09:00

193 lines
5.6 KiB
Rust

//! 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<CniResult> {
// 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<CniInterface>,
pub ips: Vec<CniIpConfig>,
pub routes: Vec<CniRoute>,
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<String>,
}