- Replace form_urlencoded with RFC 3986 compliant URI encoding - Implement aws_uri_encode() matching AWS SigV4 spec exactly - Unreserved chars (A-Z,a-z,0-9,-,_,.,~) not encoded - All other chars percent-encoded with uppercase hex - Preserve slashes in paths, encode in query params - Normalize empty paths to '/' per AWS spec - Fix test expectations (body hash, HMAC values) - Add comprehensive SigV4 signature determinism test This fixes the canonicalization mismatch that caused signature validation failures in T047. Auth can now be enabled for production. Refs: T058.S1
311 lines
9.2 KiB
Rust
311 lines
9.2 KiB
Rust
//! PrismNET CNI Plugin for k8shost
|
|
//!
|
|
//! This binary implements the CNI 1.0.0 specification to integrate k8shost pods
|
|
//! with PrismNET's OVN-based virtual networking.
|
|
//!
|
|
//! CNI operations:
|
|
//! - ADD: Create network interface and attach to OVN logical switch
|
|
//! - DEL: Remove network interface and clean up OVN resources
|
|
//! - CHECK: Verify network configuration is correct
|
|
//! - VERSION: Report supported CNI versions
|
|
|
|
use anyhow::{Context, Result};
|
|
use prismnet_api::{
|
|
port_service_client::PortServiceClient, CreatePortRequest, DeletePortRequest,
|
|
ListPortsRequest,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::io::{self, Read};
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct CniConfig {
|
|
#[serde(rename = "cniVersion")]
|
|
cni_version: String,
|
|
name: String,
|
|
#[serde(rename = "type")]
|
|
plugin_type: String,
|
|
#[serde(default)]
|
|
prismnet: PrismNETConfig,
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
struct PrismNETConfig {
|
|
server_addr: String,
|
|
subnet_id: String,
|
|
org_id: String,
|
|
project_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct CniResult {
|
|
#[serde(rename = "cniVersion")]
|
|
cni_version: String,
|
|
interfaces: Vec<Interface>,
|
|
ips: Vec<IpConfig>,
|
|
routes: Vec<Route>,
|
|
dns: DnsConfig,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct Interface {
|
|
name: String,
|
|
mac: String,
|
|
sandbox: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct IpConfig {
|
|
interface: usize,
|
|
address: String,
|
|
gateway: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct Route {
|
|
dst: String,
|
|
gw: String,
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
struct DnsConfig {
|
|
nameservers: Vec<String>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
tracing_subscriber::fmt()
|
|
.with_writer(std::io::stderr)
|
|
.init();
|
|
|
|
let command = std::env::var("CNI_COMMAND").context("CNI_COMMAND not set")?;
|
|
|
|
match command.as_str() {
|
|
"ADD" => handle_add().await,
|
|
"DEL" => handle_del().await,
|
|
"CHECK" => handle_check(),
|
|
"VERSION" => handle_version(),
|
|
_ => Err(anyhow::anyhow!("Unknown CNI command: {}", command)),
|
|
}
|
|
}
|
|
|
|
async fn handle_add() -> Result<()> {
|
|
// Read CNI config from stdin
|
|
let mut buffer = String::new();
|
|
io::stdin().read_to_string(&mut buffer)?;
|
|
let config: CniConfig = serde_json::from_str(&buffer)?;
|
|
|
|
// Parse CNI environment variables
|
|
let container_id = std::env::var("CNI_CONTAINERID").context("CNI_CONTAINERID not set")?;
|
|
let netns = std::env::var("CNI_NETNS").context("CNI_NETNS not set")?;
|
|
let ifname = std::env::var("CNI_IFNAME").context("CNI_IFNAME not set")?;
|
|
|
|
tracing::info!(
|
|
container_id = %container_id,
|
|
netns = %netns,
|
|
ifname = %ifname,
|
|
"CNI ADD operation starting"
|
|
);
|
|
|
|
// Connect to PrismNET server
|
|
let prismnet_addr = if config.prismnet.server_addr.is_empty() {
|
|
std::env::var("NOVANET_SERVER_ADDR").unwrap_or_else(|_| "http://127.0.0.1:50052".to_string())
|
|
} else {
|
|
config.prismnet.server_addr.clone()
|
|
};
|
|
|
|
let mut port_client = PortServiceClient::connect(prismnet_addr.clone())
|
|
.await
|
|
.context("Failed to connect to PrismNET server")?;
|
|
|
|
// Extract tenant context from config or environment
|
|
let org_id = if !config.prismnet.org_id.is_empty() {
|
|
config.prismnet.org_id.clone()
|
|
} else {
|
|
std::env::var("K8SHOST_ORG_ID").unwrap_or_else(|_| "default-org".to_string())
|
|
};
|
|
|
|
let project_id = if !config.prismnet.project_id.is_empty() {
|
|
config.prismnet.project_id.clone()
|
|
} else {
|
|
std::env::var("K8SHOST_PROJECT_ID").unwrap_or_else(|_| "default-project".to_string())
|
|
};
|
|
|
|
let subnet_id = if !config.prismnet.subnet_id.is_empty() {
|
|
config.prismnet.subnet_id.clone()
|
|
} else {
|
|
std::env::var("K8SHOST_SUBNET_ID").context("subnet_id not configured")?
|
|
};
|
|
|
|
// Create port in PrismNET
|
|
let port_name = format!("pod-{}", container_id);
|
|
let create_req = CreatePortRequest {
|
|
org_id: org_id.clone(),
|
|
project_id: project_id.clone(),
|
|
subnet_id: subnet_id.clone(),
|
|
name: port_name.clone(),
|
|
description: format!("k8shost pod {} network port", container_id),
|
|
ip_address: String::new(), // Let PrismNET auto-allocate
|
|
security_group_ids: vec![],
|
|
};
|
|
|
|
let create_resp = port_client
|
|
.create_port(create_req)
|
|
.await
|
|
.context("Failed to create PrismNET port")?
|
|
.into_inner();
|
|
|
|
let port = create_resp.port.context("Port not returned in response")?;
|
|
|
|
tracing::info!(
|
|
port_id = %port.id,
|
|
ip_address = %port.ip_address,
|
|
mac_address = %port.mac_address,
|
|
"PrismNET port created successfully"
|
|
);
|
|
|
|
// TODO: In production, we would:
|
|
// 1. Create veth pair
|
|
// 2. Move one end to container network namespace
|
|
// 3. Configure IP address and routes
|
|
// 4. Configure OVN logical switch port with MAC/IP
|
|
//
|
|
// For MVP, we return the allocated IP/MAC information
|
|
|
|
// Extract gateway from subnet (would come from GetSubnet call in production)
|
|
let gateway = port.ip_address.split('.').take(3).collect::<Vec<_>>().join(".") + ".1";
|
|
|
|
// Return CNI result
|
|
let result = CniResult {
|
|
cni_version: config.cni_version,
|
|
interfaces: vec![Interface {
|
|
name: ifname.clone(),
|
|
mac: port.mac_address.clone(),
|
|
sandbox: netns,
|
|
}],
|
|
ips: vec![IpConfig {
|
|
interface: 0,
|
|
address: format!("{}/24", port.ip_address),
|
|
gateway: gateway.clone(),
|
|
}],
|
|
routes: vec![Route {
|
|
dst: "0.0.0.0/0".to_string(),
|
|
gw: gateway,
|
|
}],
|
|
dns: DnsConfig {
|
|
nameservers: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()],
|
|
},
|
|
};
|
|
|
|
println!("{}", serde_json::to_string(&result)?);
|
|
|
|
tracing::info!("CNI ADD operation completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_del() -> Result<()> {
|
|
// Read CNI config from stdin
|
|
let mut buffer = String::new();
|
|
io::stdin().read_to_string(&mut buffer)?;
|
|
let config: CniConfig = serde_json::from_str(&buffer)?;
|
|
|
|
// Parse CNI environment variables
|
|
let container_id = std::env::var("CNI_CONTAINERID").context("CNI_CONTAINERID not set")?;
|
|
|
|
tracing::info!(
|
|
container_id = %container_id,
|
|
"CNI DEL operation starting"
|
|
);
|
|
|
|
// Connect to PrismNET server
|
|
let prismnet_addr = if config.prismnet.server_addr.is_empty() {
|
|
std::env::var("NOVANET_SERVER_ADDR").unwrap_or_else(|_| "http://127.0.0.1:50052".to_string())
|
|
} else {
|
|
config.prismnet.server_addr.clone()
|
|
};
|
|
|
|
let mut port_client = PortServiceClient::connect(prismnet_addr.clone())
|
|
.await
|
|
.context("Failed to connect to PrismNET server")?;
|
|
|
|
// Extract tenant context
|
|
let org_id = if !config.prismnet.org_id.is_empty() {
|
|
config.prismnet.org_id.clone()
|
|
} else {
|
|
std::env::var("K8SHOST_ORG_ID").unwrap_or_else(|_| "default-org".to_string())
|
|
};
|
|
|
|
let project_id = if !config.prismnet.project_id.is_empty() {
|
|
config.prismnet.project_id.clone()
|
|
} else {
|
|
std::env::var("K8SHOST_PROJECT_ID").unwrap_or_else(|_| "default-project".to_string())
|
|
};
|
|
|
|
let subnet_id = if !config.prismnet.subnet_id.is_empty() {
|
|
config.prismnet.subnet_id.clone()
|
|
} else {
|
|
std::env::var("K8SHOST_SUBNET_ID").unwrap_or_default()
|
|
};
|
|
|
|
// Find port by container ID using device_id filter
|
|
// List ports to find our port ID
|
|
let list_req = ListPortsRequest {
|
|
org_id: org_id.clone(),
|
|
project_id: project_id.clone(),
|
|
subnet_id: subnet_id.clone(),
|
|
device_id: container_id.clone(),
|
|
page_size: 10,
|
|
page_token: String::new(),
|
|
};
|
|
|
|
let list_resp = port_client.list_ports(list_req).await;
|
|
|
|
if let Ok(resp) = list_resp {
|
|
let ports = resp.into_inner().ports;
|
|
if let Some(port) = ports.first() {
|
|
// Delete the port
|
|
let delete_req = DeletePortRequest {
|
|
org_id,
|
|
project_id,
|
|
subnet_id,
|
|
id: port.id.clone(),
|
|
};
|
|
|
|
port_client
|
|
.delete_port(delete_req)
|
|
.await
|
|
.context("Failed to delete PrismNET port")?;
|
|
|
|
tracing::info!(
|
|
port_id = %port.id,
|
|
"PrismNET port deleted successfully"
|
|
);
|
|
}
|
|
}
|
|
|
|
// TODO: In production, we would also:
|
|
// 1. Remove network interfaces from container namespace
|
|
// 2. Clean up veth pair
|
|
// 3. Remove OVN logical switch port configuration
|
|
|
|
tracing::info!("CNI DEL operation completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_check() -> Result<()> {
|
|
// TODO: Implement CHECK logic
|
|
// Verify that the network configuration is still valid
|
|
// For now, return success
|
|
|
|
tracing::info!("CNI CHECK operation - basic validation passed");
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_version() -> Result<()> {
|
|
let version = serde_json::json!({
|
|
"cniVersion": "1.0.0",
|
|
"supportedVersions": ["0.3.0", "0.3.1", "0.4.0", "1.0.0"]
|
|
});
|
|
|
|
println!("{}", serde_json::to_string(&version)?);
|
|
Ok(())
|
|
}
|