- 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)
193 lines
5.6 KiB
Rust
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>,
|
|
}
|