photoncloud-monorepo/k8shost/crates/k8shost-cni/src/main.rs
centra d2149b6249 fix(lightningstor): Fix SigV4 canonicalization for AWS S3 auth
- 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
2025-12-12 06:23:46 +09:00

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(())
}