use std::{collections::HashMap, io::Read, path::PathBuf, str::FromStr};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
error::Error,
util::{Env, Io},
};
pub(super) const CNI_COMMAND: &str = "CNI_COMMAND";
pub(super) const CNI_CONTAINERID: &str = "CNI_CONTAINERID";
pub(super) const CNI_NETNS: &str = "CNI_NETNS";
pub(super) const CNI_IFNAME: &str = "CNI_IFNAME";
pub(super) const CNI_ARGS: &str = "CNI_ARGS";
pub(super) const CNI_PATH: &str = "CNI_PATH";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Cmd {
Add,
Del,
Check,
Gc,
Status,
Version,
UnSet,
}
impl FromStr for Cmd {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ADD" => Ok(Self::Add),
"DEL" => Ok(Self::Del),
"CHECK" => Ok(Self::Check),
"GC" => Ok(Self::Gc),
"STATUS" => Ok(Self::Status),
"VERSION" => Ok(Self::Version),
"" => Ok(Self::UnSet),
_ => Err(Error::InvalidEnvValue(format!("unknown CNI_COMMAND: {s}"))),
}
}
}
impl From<Cmd> for &str {
fn from(cmd: Cmd) -> Self {
match cmd {
Cmd::Add => "ADD",
Cmd::Del => "DEL",
Cmd::Check => "CHECK",
Cmd::Gc => "GC",
Cmd::Status => "STATUS",
Cmd::Version => "VERSION",
Cmd::UnSet => "",
}
}
}
impl Cmd {
pub(super) fn get_from_env<E: Env>() -> Result<Self, Error> {
Self::from_str(&E::get::<String>(CNI_COMMAND)?)
}
}
#[derive(Debug, Default, Clone)]
pub struct Args {
container_id: Option<String>,
netns: Option<PathBuf>,
ifname: Option<String>,
#[allow(clippy::struct_field_names)]
args: Option<String>,
path: Vec<PathBuf>,
config: Option<NetConf>,
}
impl Args {
#[must_use]
pub fn container_id(&self) -> Option<&str> {
self.container_id.as_deref()
}
#[must_use]
pub const fn netns(&self) -> Option<&PathBuf> {
self.netns.as_ref()
}
#[must_use]
pub fn ifname(&self) -> Option<&str> {
self.ifname.as_deref()
}
#[must_use]
pub fn args(&self) -> Option<&str> {
self.args.as_deref()
}
#[must_use]
pub fn path(&self) -> &[PathBuf] {
&self.path
}
#[must_use]
pub const fn config(&self) -> Option<&NetConf> {
self.config.as_ref()
}
}
#[derive(Debug)]
pub struct ArgsBuilder<E: Env, I: Io> {
container_id: Option<String>,
netns: Option<PathBuf>,
ifname: Option<String>,
#[allow(clippy::struct_field_names)]
args: Option<String>,
path: Vec<PathBuf>,
config: Option<NetConf>,
_phantom_e: std::marker::PhantomData<E>,
_phantom_i: std::marker::PhantomData<I>,
}
impl<E: Env, I: Io> ArgsBuilder<E, I> {
#[must_use]
pub const fn new() -> Self {
Self {
container_id: None,
netns: None,
ifname: None,
args: None,
path: Vec::new(),
config: None,
_phantom_e: std::marker::PhantomData,
_phantom_i: std::marker::PhantomData,
}
}
pub fn container_id(mut self) -> Result<Self, Error> {
match E::get::<String>(CNI_CONTAINERID) {
Ok(val) => self.container_id = Some(val),
Err(e) => return Err(e),
}
Ok(self)
}
pub fn netns(mut self) -> Result<Self, Error> {
match E::get::<String>(CNI_NETNS) {
Ok(val) => {
self.netns = PathBuf::from_str(&val)
.map_err(|e| Error::FailedToDecode(e.to_string()))
.ok();
}
Err(e) => return Err(e),
}
Ok(self)
}
pub fn ifname(mut self) -> Result<Self, Error> {
match E::get::<String>(CNI_IFNAME) {
Ok(val) => self.ifname = Some(val),
Err(e) => return Err(e),
}
Ok(self)
}
pub fn args(mut self) -> Result<Self, Error> {
match E::get::<String>(CNI_ARGS) {
Ok(val) => self.args = if val.is_empty() { None } else { Some(val) },
Err(e) => return Err(e),
}
Ok(self)
}
pub fn path(mut self) -> Result<Self, Error> {
match E::get::<String>(CNI_PATH) {
Ok(val) => self.path = val.split(':').map(PathBuf::from).collect(),
Err(e) => return Err(e),
}
Ok(self)
}
pub fn config(mut self) -> Result<Self, Error> {
let mut buf = String::new();
I::io_in()
.read_to_string(&mut buf)
.map_err(|e| Error::IOFailure(e.to_string()))?;
self.config =
serde_json::from_str(&buf).map_err(|e| Error::FailedToDecode(e.to_string()))?;
Ok(self)
}
pub(crate) fn validate(self, cmd: Cmd) -> Result<Self, Error> {
match cmd {
Cmd::Add | Cmd::Del | Cmd::Check => {
if self.container_id.is_none() {
return Err(Error::InvalidEnvValue(
"CNI_CONTAINERID is required for ADD/DEL/CHECK commands".to_string(),
));
}
if self.ifname.is_none() {
return Err(Error::InvalidEnvValue(
"CNI_IFNAME is required for ADD/DEL/CHECK commands".to_string(),
));
}
}
Cmd::Gc => {
if self.path.is_empty() {
return Err(Error::InvalidEnvValue(
"CNI_PATH is required for GC command".to_string(),
));
}
}
Cmd::Status | Cmd::Version | Cmd::UnSet => {
}
}
Ok(self)
}
pub fn build(self) -> Result<Args, Error> {
Ok(Args {
container_id: self.container_id,
netns: self.netns,
ifname: self.ifname,
args: self.args,
path: self.path,
config: self.config,
})
}
}
impl<E: Env, I: Io> Default for ArgsBuilder<E, I> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct NetConf {
pub cni_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cni_versions: Option<Vec<String>>,
pub name: String,
pub r#type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_check: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_config: Option<RuntimeConf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<HashMap<String, Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ip_masq: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ipam: Option<Ipam>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<Dns>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub args: Option<HashMap<String, Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prev_result: Option<CNIResult>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "cni.dev/valid-attachments"
)]
pub valid_attachments: Option<Vec<GcAttachment>>,
#[serde(flatten)]
pub custom: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetConfList {
pub cni_version: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cni_versions: Vec<String>,
pub name: String,
#[serde(default)] pub disable_check: bool,
#[serde(default)] pub disable_gc: bool,
#[serde(default)] pub load_only_inlined_plugins: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub plugins: Vec<NetConf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GcAttachment {
pub container_id: String,
pub ifname: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeConf {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub port_mappings: Vec<PortMapping>,
#[serde(flatten)]
pub custom: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Ipam {
pub r#type: String,
#[serde(flatten)]
pub custom: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Dns {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nameservers: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub search: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Route {
pub dst: String, #[serde(skip_serializing_if = "Option::is_none")]
pub gw: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtu: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub advmss: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub table: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct CNIResult {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub interfaces: Vec<Interface>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ips: Vec<IpConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routes: Vec<Route>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<Dns>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CNIResultWithCNIVersion {
pub cni_version: String,
#[serde(flatten)]
inner: CNIResult,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Interface {
pub name: String,
pub mac: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtu: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sandbox: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub socket_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "pciID")]
pub pci_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct IpConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub interface: Option<u32>,
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub gateway: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PortMapping {
pub host_port: u32,
pub container_port: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol: Option<Protocol>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Protocol {
Tcp,
Udp,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ErrorResult {
pub(crate) cni_version: String,
pub(crate) code: u32,
pub(crate) msg: String,
pub(crate) details: String,
}
impl From<&ErrorResult> for Error {
fn from(res: &ErrorResult) -> Self {
if res.code > 100 {
return Self::Custom(res.code, res.msg.clone(), res.details.clone());
}
match res.code {
1 => Self::IncompatibleVersion(res.details.clone()),
2 => Self::UnsupportedNetworkConfiguration(res.details.clone()),
3 => Self::NotExist(res.details.clone()),
4 => Self::InvalidEnvValue(res.details.clone()),
5 => Self::IOFailure(res.details.clone()),
6 => Self::FailedToDecode(res.details.clone()),
7 => Self::InvalidNetworkConfig(res.details.clone()),
11 => Self::TryAgainLater(res.details.clone()),
50 => Self::PluginNotAvailable(res.details.clone()),
51 => Self::PluginNotAvailableLimitedConnectivity(res.details.clone()),
_ => Self::FailedToDecode(format!("unknown error code: {}", res.code)),
}
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, str::FromStr, vec};
use rstest::rstest;
use serde_json::json;
use crate::error::Error;
use super::{
CNIResult, Cmd, Dns, GcAttachment, Interface, IpConfig, Ipam, NetConf, PortMapping,
Protocol, Route, RuntimeConf,
};
#[rstest]
#[case("ADD", Cmd::Add)]
#[case("DEL", Cmd::Del)]
#[case("CHECK", Cmd::Check)]
#[case("GC", Cmd::Gc)]
#[case("STATUS", Cmd::Status)]
#[case("VERSION", Cmd::Version)]
#[case("", Cmd::UnSet)]
fn test_cmd_from_str(#[case] input: &str, #[case] expected: Cmd) {
let result = Cmd::from_str(input);
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected);
}
#[test]
fn test_cmd_from_str_invalid() {
let result = Cmd::from_str("INVALID");
assert!(result.is_err());
if let Err(Error::InvalidEnvValue(msg)) = result {
assert!(msg.contains("unknown CNI_COMMAND"));
} else {
panic!("Expected InvalidEnvValue error");
}
}
#[rstest]
#[case(Cmd::Add, "ADD")]
#[case(Cmd::Del, "DEL")]
#[case(Cmd::Check, "CHECK")]
#[case(Cmd::Gc, "GC")]
#[case(Cmd::Version, "VERSION")]
#[case(Cmd::UnSet, "")]
fn test_cmd_to_str(#[case] cmd: Cmd, #[case] expected: &str) {
let result: &str = cmd.into();
assert_eq!(result, expected);
}
#[test]
fn test_gc_attachment_serialize() {
let attachment = GcAttachment {
container_id: "container-123".to_string(),
ifname: "eth0".to_string(),
};
let json = serde_json::to_string(&attachment).unwrap();
let expected = r#"{"container_id":"container-123","ifname":"eth0"}"#;
assert_eq!(json, expected);
let deserialized: GcAttachment = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.container_id, "container-123");
assert_eq!(deserialized.ifname, "eth0");
}
#[test]
fn test_net_conf_with_valid_attachments() {
let json = r#"{
"cniVersion": "1.3.0",
"name": "test-network",
"type": "bridge",
"cni.dev/valid-attachments": [
{"container_id": "container-1", "ifname": "eth0"},
{"container_id": "container-2", "ifname": "eth1"}
]
}"#;
let conf: NetConf = serde_json::from_str(json).unwrap();
assert_eq!(conf.cni_version, "1.3.0");
assert_eq!(conf.name, "test-network");
assert_eq!(conf.r#type, "bridge");
assert!(conf.valid_attachments.is_some());
let attachments = conf.valid_attachments.unwrap();
assert_eq!(attachments.len(), 2);
assert_eq!(attachments[0].container_id, "container-1");
assert_eq!(attachments[0].ifname, "eth0");
assert_eq!(attachments[1].container_id, "container-2");
assert_eq!(attachments[1].ifname, "eth1");
let conf_with_attachments = NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "test-network".to_string(),
r#type: "bridge".to_string(),
disable_check: None,
runtime_config: None,
capabilities: None,
ip_masq: None,
ipam: None,
dns: None,
args: None,
prev_result: None,
valid_attachments: Some(vec![GcAttachment {
container_id: "container-1".to_string(),
ifname: "eth0".to_string(),
}]),
custom: HashMap::new(),
};
let serialized = serde_json::to_value(&conf_with_attachments).unwrap();
assert!(serialized["cni.dev/valid-attachments"].is_array());
assert_eq!(
serialized["cni.dev/valid-attachments"][0]["container_id"],
"container-1"
);
}
#[rstest(
input,
expected,
case(
// ref: https://github.com/containernetworking/cni/blob/b62753aa2bfa365c1ceaff6f25774a8047c896b5/SPEC.md#add-example
r#"{
"cniVersion": "1.3.0",
"name": "dbnet",
"type": "bridge",
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "dbnet".to_string(),
r#type: "bridge".to_string(),
disable_check: None,
runtime_config: None,
capabilities: None,
ip_masq: None,
ipam: Some(Ipam{
r#type: "host-local".to_string(),
custom: HashMap::from([
("subnet".to_string(), serde_json::Value::String("10.1.0.0/16".to_string())),
("gateway".to_string(), serde_json::Value::String("10.1.0.1".to_string())),
]),
}),
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
args: None,
prev_result: None,
custom: HashMap::from([
("bridge".to_string(), serde_json::Value::String("cni0".to_string())),
("keyA".to_string(), serde_json::Value::Array(vec![serde_json::Value::String("some more".to_string()), serde_json::Value::String("plugin specific".to_string()), serde_json::Value::String("configuration".to_string())])),
]),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "test",
"type": "myPlugin",
"capabilities": {
"portMappings": true
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "test".to_string(),
r#type: "myPlugin".to_string(),
disable_check: None,
runtime_config: None,
capabilities: Some(HashMap::from([
("portMappings".to_string(), serde_json::Value::Bool(true)),
])),
ip_masq: None,
ipam: None,
dns: None,
args: None,
prev_result: None,
custom: HashMap::new(),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "test",
"type": "myPlugin",
"capabilities": {
"portMappings": true
},
"runtimeConfig": {
"portMappings": [ { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" } ]
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "test".to_string(),
r#type: "myPlugin".to_string(),
disable_check: None,
runtime_config: Some(RuntimeConf{
port_mappings: vec![
PortMapping{
host_port: 8080,
container_port: 80,
protocol: Some(Protocol::Tcp),
},
],
custom: HashMap::new(),
}),
capabilities: Some(HashMap::from([
("portMappings".to_string(), serde_json::Value::Bool(true)),
])),
ip_masq: None,
ipam: None,
dns: None,
args: None,
prev_result: None,
custom: HashMap::new(),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "dbnet",
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
},
"runtimeConfig": {
"mac": "00:11:22:33:44:66"
},
"prevResult": {
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "99:88:77:66:55:44",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "dbnet".to_string(),
r#type: "tuning".to_string(),
disable_check: None,
runtime_config: Some(RuntimeConf{
port_mappings: Vec::new(),
custom: HashMap::from([("mac".to_string(), serde_json::Value::String("00:11:22:33:44:66".to_string()))])
}),
capabilities: None,
ip_masq: None,
ipam: None,
dns: None,
args: None,
prev_result: Some(CNIResult{
ips: vec![
IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway:Some("10.1.0.1".to_string()),
},
],
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "99:88:77:66:55:44".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
}),
custom: HashMap::from([
("sysctl".to_string(), json!({"net.core.somaxconn": "500"})),
]),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "dbnet",
"type": "portmap",
"runtimeConfig": {
"portMappings" : [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp" }
]
},
"prevResult": {
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "dbnet".to_string(),
r#type: "portmap".to_string(),
disable_check: None,
runtime_config: Some(RuntimeConf{
port_mappings: vec![
PortMapping{
host_port: 8080,
container_port: 80,
protocol: Some(Protocol::Tcp),
},
],
custom: HashMap::new(),
}),
capabilities: None,
ip_masq: None,
ipam: None,
dns: None,
args: None,
prev_result: Some(CNIResult{
ips: vec![
IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway:Some("10.1.0.1".to_string()),
},
],
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "00:11:22:33:44:66".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
}),
custom: HashMap::new(),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "dbnet",
"type": "portmap",
"runtimeConfig": {
"portMappings" : [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp" }
]
},
"prevResult": {
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "dbnet".to_string(),
r#type: "portmap".to_string(),
disable_check: None,
runtime_config: Some(RuntimeConf{
port_mappings: vec![
PortMapping{
host_port: 8080,
container_port: 80,
protocol: Some(Protocol::Tcp),
},
],
custom: HashMap::new(),
}),
capabilities: None,
ip_masq: None,
ipam: None,
dns: None,
args: None,
prev_result: Some(CNIResult{
ips: vec![
IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway:Some("10.1.0.1".to_string()),
},
],
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "00:11:22:33:44:66".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
}),
custom: HashMap::new(),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "dbnet",
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
},
"runtimeConfig": {
"mac": "00:11:22:33:44:66"
},
"prevResult": {
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "dbnet".to_string(),
r#type: "tuning".to_string(),
disable_check: None,
runtime_config: Some(RuntimeConf{
port_mappings: Vec::new(),
custom: HashMap::from([("mac".to_string(), serde_json::Value::String("00:11:22:33:44:66".to_string()))])
}),
capabilities: None,
ip_masq: None,
ipam: None,
dns: None,
args: None,
prev_result: Some(CNIResult{
ips: vec![
IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway:Some("10.1.0.1".to_string()),
},
],
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "00:11:22:33:44:66".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
}),
custom: HashMap::from([
("sysctl".to_string(), json!({"net.core.somaxconn": "500"})),
]),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "dbnet",
"type": "bridge",
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
},
"prevResult": {
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "dbnet".to_string(),
r#type: "bridge".to_string(),
disable_check: None,
runtime_config: None,
capabilities: None,
ip_masq: None,
ipam: Some(Ipam{
r#type: "host-local".to_string(),
custom: HashMap::from([
("subnet".to_string(), serde_json::Value::String("10.1.0.0/16".to_string())),
("gateway".to_string(), serde_json::Value::String("10.1.0.1".to_string())),
]),
}),
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None
}),
args: None,
prev_result: Some(CNIResult{
ips: vec![
IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway:Some("10.1.0.1".to_string()),
},
],
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "00:11:22:33:44:66".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
}),
custom: HashMap::from([
("bridge".to_string(), serde_json::Value::String("cni0".to_string())),
("keyA".to_string(), serde_json::Value::Array(vec![serde_json::Value::String("some more".to_string()), serde_json::Value::String("plugin specific".to_string()), serde_json::Value::String("configuration".to_string())])),
]),
valid_attachments: None,
},
),
case(
r#"{
"cniVersion": "1.3.0",
"name": "dbnet",
"type": "bridge",
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
},
"prevResult": {
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "00:11:22:33:44:66",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}
}"#.to_string(),
NetConf {
cni_version: "1.3.0".to_string(),
cni_versions: None,
name: "dbnet".to_string(),
r#type: "bridge".to_string(),
disable_check: None,
runtime_config: None,
capabilities: None,
ip_masq: None,
ipam: Some(Ipam{
r#type: "host-local".to_string(),
custom: HashMap::from([
("subnet".to_string(), serde_json::Value::String("10.1.0.0/16".to_string())),
("gateway".to_string(), serde_json::Value::String("10.1.0.1".to_string())),
]),
}),
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None
}),
args: None,
prev_result: Some(CNIResult{
ips: vec![
IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway:Some("10.1.0.1".to_string()),
},
],
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "00:11:22:33:44:66".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
}),
custom: HashMap::from([
("keyA".to_string(), serde_json::Value::Array(vec![serde_json::Value::String("some more".to_string()), serde_json::Value::String("plugin specific".to_string()), serde_json::Value::String("configuration".to_string())])),
("bridge".to_string(), serde_json::Value::String("cni0".to_string())),
]),
valid_attachments: None,
},
),
)]
fn deserialize_and_serialize_net_conf(input: String, expected: NetConf) {
let conf: NetConf = serde_json::from_str(&input).unwrap();
assert_eq!(expected, conf);
let data = serde_json::to_string_pretty(&conf).unwrap();
let conf_again: NetConf = serde_json::from_str(&data).unwrap();
assert_eq!(expected, conf_again);
}
#[rstest(
input,
expected,
case(
// ref: https://github.com/containernetworking/cni/blob/b62753aa2bfa365c1ceaff6f25774a8047c896b5/SPEC.md#add-example
r#"{
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1"
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}"#.to_string(),
CNIResult{
interfaces: Vec::new(),
ips: vec![IpConfig{
interface: None,
address: "10.1.0.5/16".to_string(),
gateway: Some("10.1.0.1".to_string()),
}],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
})
},
),
case(
// ref: https://github.com/containernetworking/cni/blob/b62753aa2bfa365c1ceaff6f25774a8047c896b5/SPEC.md#add-example
r#"{
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "99:88:77:66:55:44",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}"#.to_string(),
CNIResult{
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "99:88:77:66:55:44".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
ips: vec![IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway: Some("10.1.0.1".to_string()),
}],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
}),
},
),
case(
// ref: https://github.com/containernetworking/cni/blob/b62753aa2bfa365c1ceaff6f25774a8047c896b5/SPEC.md#add-example
r#"{
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 2
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"interfaces": [
{
"name": "cni0",
"mac": "00:11:22:33:44:55"
},
{
"name": "veth3243",
"mac": "55:44:33:22:11:11"
},
{
"name": "eth0",
"mac": "99:88:77:66:55:44",
"sandbox": "/var/run/netns/blue"
}
],
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}"#.to_string(),
CNIResult{
interfaces: vec![
Interface{
name: "cni0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "veth3243".to_string(),
mac: "55:44:33:22:11:11".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
},
Interface{
name: "eth0".to_string(),
mac: "99:88:77:66:55:44".to_string(),
sandbox: Some("/var/run/netns/blue".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
},
],
ips: vec![IpConfig{
interface: Some(2),
address: "10.1.0.5/16".to_string(),
gateway: Some("10.1.0.1".to_string()),
}],
routes: vec![
Route{
dst: "0.0.0.0/0".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
],
dns: Some(Dns{
nameservers: vec!["10.1.0.1".to_string()],
domain: None,
search: None,
options: None,
})
},
),
)]
fn deserialize_and_serialize_success_result(input: String, expected: CNIResult) {
let result: CNIResult = serde_json::from_str(&input).unwrap();
assert_eq!(expected, result);
let data = serde_json::to_string_pretty(&result).unwrap();
let result_again: CNIResult = serde_json::from_str(&data).unwrap();
assert_eq!(expected, result_again);
}
#[rstest]
#[case(
Interface {
name: "eth0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: Some("/var/run/netns/test".to_string()),
mtu: None,
socket_path: None,
pci_id: None,
}
)]
#[case(
Interface {
name: "veth0".to_string(),
mac: "aa:bb:cc:dd:ee:ff".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
}
)]
#[case(
Interface {
name: "eth1".to_string(),
mac: "00:11:22:33:44:66".to_string(),
mtu: Some(1500),
sandbox: Some("/var/run/netns/test".to_string()),
socket_path: None,
pci_id: None,
}
)]
#[case(
Interface {
name: "tap0".to_string(),
mac: "aa:bb:cc:dd:ee:ff".to_string(),
mtu: None,
sandbox: None,
socket_path: Some("/var/run/vhost-user/tap0.sock".to_string()),
pci_id: None,
}
)]
#[case(
Interface {
name: "net0".to_string(),
mac: "11:22:33:44:55:66".to_string(),
mtu: Some(9000),
sandbox: Some("/var/run/netns/container1".to_string()),
socket_path: None,
pci_id: Some("0000:03:00.0".to_string()),
}
)]
#[case(
Interface {
name: "eth2".to_string(),
mac: "ff:ee:dd:cc:bb:aa".to_string(),
mtu: Some(1500),
sandbox: Some("/var/run/netns/test".to_string()),
socket_path: Some("/var/run/socket/eth1.sock".to_string()),
pci_id: Some("0000:04:00.1".to_string()),
}
)]
fn test_interface_serialize(#[case] interface: Interface) {
let json = serde_json::to_string(&interface).unwrap();
assert!(json.contains(&format!("\"name\":\"{}\"", interface.name)));
assert!(json.contains(&format!("\"mac\":\"{}\"", interface.mac)));
if let Some(ref mtu) = interface.mtu {
assert!(json.contains(&format!("\"mtu\":{}", mtu)));
} else {
assert!(!json.contains("\"mtu\""));
}
if let Some(ref sandbox) = interface.sandbox {
assert!(json.contains(&format!("\"sandbox\":\"{}\"", sandbox)));
} else {
assert!(!json.contains("\"sandbox\""));
}
if let Some(ref socket_path) = interface.socket_path {
assert!(json.contains(&format!("\"socketPath\":\"{}\"", socket_path)));
} else {
assert!(!json.contains("\"socketPath\""));
}
if let Some(ref pci_id) = interface.pci_id {
assert!(json.contains(&format!("\"pciID\":\"{}\"", pci_id)));
} else {
assert!(!json.contains("\"pciID\""));
}
let deserialized: Interface = serde_json::from_str(&json).unwrap();
assert_eq!(interface, deserialized);
}
#[rstest]
#[case(
IpConfig {
interface: Some(0),
address: "192.168.1.10/24".to_string(),
gateway: Some("192.168.1.1".to_string()),
},
true,
true
)]
#[case(
IpConfig {
interface: None,
address: "10.0.0.1/8".to_string(),
gateway: None,
},
false,
false
)]
fn test_ip_config_serialize(
#[case] ip_config: IpConfig,
#[case] has_interface: bool,
#[case] has_gateway: bool,
) {
let json = serde_json::to_string(&ip_config).unwrap();
if !has_interface {
assert!(!json.contains("interface"));
}
if !has_gateway {
assert!(!json.contains("gateway"));
}
let deserialized: IpConfig = serde_json::from_str(&json).unwrap();
assert_eq!(ip_config, deserialized);
}
#[rstest]
#[case(
Route {
dst: "0.0.0.0/0".to_string(),
gw: Some("192.168.1.1".to_string()),
mtu: Some(1500),
advmss: Some(1460),
priority: None,
table: None,
scope: None,
},
true,
true
)]
#[case(
Route {
dst: "10.0.0.0/8".to_string(),
gw: None,
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
},
false,
false
)]
fn test_route_serialize(#[case] route: Route, #[case] has_gw: bool, #[case] has_mtu: bool) {
let json = serde_json::to_string(&route).unwrap();
if !has_gw {
assert!(!json.contains("\"gw\""));
}
if !has_mtu {
assert!(!json.contains("\"mtu\""));
}
let deserialized: Route = serde_json::from_str(&json).unwrap();
assert_eq!(route, deserialized);
}
#[rstest]
#[case(
Dns {
nameservers: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()],
domain: Some("example.com".to_string()),
search: Some(vec!["example.com".to_string(), "test.com".to_string()]),
options: Some(vec!["ndots:5".to_string()]),
},
true,
true,
true
)]
#[case(
Dns {
nameservers: vec!["1.1.1.1".to_string()],
domain: None,
search: None,
options: None,
},
false,
false,
false
)]
fn test_dns_serialize(
#[case] dns: Dns,
#[case] has_domain: bool,
#[case] has_search: bool,
#[case] has_options: bool,
) {
let json = serde_json::to_string(&dns).unwrap();
if !has_domain {
assert!(!json.contains("\"domain\""));
}
if !has_search {
assert!(!json.contains("\"search\""));
}
if !has_options {
assert!(!json.contains("\"options\""));
}
let deserialized: Dns = serde_json::from_str(&json).unwrap();
assert_eq!(dns, deserialized);
}
#[rstest]
#[case(Protocol::Tcp, "tcp")]
#[case(Protocol::Udp, "udp")]
fn test_protocol_serialize(#[case] protocol: Protocol, #[case] expected: &str) {
let json = serde_json::to_string(&protocol).unwrap();
assert_eq!(json, format!("\"{}\"", expected));
let deserialized: Protocol = serde_json::from_str(&json).unwrap();
assert_eq!(protocol, deserialized);
}
#[rstest]
#[case(
PortMapping {
host_port: 8080,
container_port: 80,
protocol: Some(Protocol::Tcp),
},
true
)]
#[case(
PortMapping {
host_port: 443,
container_port: 443,
protocol: None,
},
false
)]
fn test_port_mapping_serialize(#[case] port_mapping: PortMapping, #[case] has_protocol: bool) {
let json = serde_json::to_string(&port_mapping).unwrap();
if !has_protocol {
assert!(!json.contains("\"protocol\""));
}
let deserialized: PortMapping = serde_json::from_str(&json).unwrap();
assert_eq!(port_mapping, deserialized);
}
#[rstest]
#[case(CNIResult::default(), false, false, false, false)]
#[case(
CNIResult {
interfaces: vec![Interface {
name: "eth0".to_string(),
mac: "00:11:22:33:44:55".to_string(),
sandbox: None,
mtu: None,
socket_path: None,
pci_id: None,
}],
ips: vec![IpConfig {
interface: Some(0),
address: "192.168.1.10/24".to_string(),
gateway: Some("192.168.1.1".to_string()),
}],
routes: vec![Route {
dst: "0.0.0.0/0".to_string(),
gw: Some("192.168.1.1".to_string()),
mtu: None,
advmss: None,
priority: None,
table: None,
scope: None,
}],
dns: Some(Dns {
nameservers: vec!["8.8.8.8".to_string()],
domain: None,
search: None,
options: None,
}),
},
true,
true,
true,
true
)]
fn test_cni_result_serialize(
#[case] result: CNIResult,
#[case] has_interfaces: bool,
#[case] has_ips: bool,
#[case] has_routes: bool,
#[case] has_dns: bool,
) {
let json = serde_json::to_string(&result).unwrap();
if !has_interfaces {
assert!(!json.contains("\"interfaces\""));
}
if !has_ips {
assert!(!json.contains("\"ips\""));
}
if !has_routes {
assert!(!json.contains("\"routes\""));
}
if !has_dns {
assert!(!json.contains("\"dns\""));
}
let deserialized: CNIResult = serde_json::from_str(&json).unwrap();
assert_eq!(result, deserialized);
}
#[rstest]
#[case(
Ipam {
r#type: "host-local".to_string(),
custom: HashMap::from([
("subnet".to_string(), json!("10.1.0.0/16")),
("gateway".to_string(), json!("10.1.0.1")),
]),
},
vec!["subnet", "gateway"]
)]
#[case(
Ipam {
r#type: "dhcp".to_string(),
custom: HashMap::new(),
},
vec![]
)]
fn test_ipam_serialize(#[case] ipam: Ipam, #[case] expected_keys: Vec<&str>) {
let json = serde_json::to_string(&ipam).unwrap();
let deserialized: Ipam = serde_json::from_str(&json).unwrap();
assert_eq!(ipam.r#type, deserialized.r#type);
for key in expected_keys {
assert_eq!(ipam.custom.get(key), deserialized.custom.get(key));
}
}
#[rstest]
#[case(
RuntimeConf {
port_mappings: vec![PortMapping {
host_port: 8080,
container_port: 80,
protocol: Some(Protocol::Tcp),
}],
custom: HashMap::new(),
}
)]
#[case(
RuntimeConf {
port_mappings: vec![],
custom: HashMap::from([("mac".to_string(), json!("00:11:22:33:44:66"))]),
}
)]
fn test_runtime_conf_serialize(#[case] runtime_conf: RuntimeConf) {
let json = serde_json::to_string(&runtime_conf).unwrap();
let deserialized: RuntimeConf = serde_json::from_str(&json).unwrap();
assert_eq!(runtime_conf, deserialized);
}
#[rstest]
#[case(
r#"{
"cniVersion": "1.0.0",
"interfaces": [
{
"name": "eth0",
"mac": "00:11:22:33:44:55",
"sandbox": "/var/run/netns/test"
}
],
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1",
"interface": 0
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {
"nameservers": ["10.1.0.1"]
}
}"#,
"1.0.0",
1,
1,
1,
true
)]
#[case(
r#"{
"cniVersion": "1.3.0",
"interfaces": [],
"ips": [],
"routes": []
}"#,
"1.3.0",
0,
0,
0,
false
)]
fn test_cni_result_with_cni_version(
#[case] input: &str,
#[case] expected_version: &str,
#[case] expected_interfaces: usize,
#[case] expected_ips: usize,
#[case] expected_routes: usize,
#[case] has_dns: bool,
) {
let result: super::CNIResultWithCNIVersion = serde_json::from_str(input).unwrap();
assert_eq!(result.cni_version, expected_version);
assert_eq!(result.inner.interfaces.len(), expected_interfaces);
assert_eq!(result.inner.ips.len(), expected_ips);
assert_eq!(result.inner.routes.len(), expected_routes);
assert_eq!(result.inner.dns.is_some(), has_dns);
}
#[test]
fn test_ipam_delegated_plugin_result() {
let input = r#"{
"ips": [
{
"address": "10.1.0.5/16",
"gateway": "10.1.0.1"
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {
"nameservers": ["10.1.0.1"]
}
}"#;
let result: CNIResult = serde_json::from_str(input).unwrap();
assert!(result.interfaces.is_empty());
assert_eq!(result.ips.len(), 1);
assert!(
result.ips[0].interface.is_none(),
"IPAM result should not have interface field in ips"
);
assert_eq!(result.routes.len(), 1);
assert!(result.dns.is_some());
}
#[rstest]
#[case(
Route {
dst: "192.168.0.0/16".to_string(),
gw: Some("10.1.0.1".to_string()),
mtu: Some(1500),
advmss: Some(1460),
priority: None,
table: None,
scope: None,
},
true,
true
)]
#[case(
Route {
dst: "10.0.0.0/8".to_string(),
gw: None,
mtu: Some(9000),
advmss: None,
priority: None,
table: None,
scope: None,
},
true,
false
)]
fn test_route_with_mtu_and_advmss(
#[case] route: Route,
#[case] has_mtu: bool,
#[case] has_advmss: bool,
) {
let json = serde_json::to_string(&route).unwrap();
if has_mtu {
assert!(json.contains("\"mtu\""));
}
if has_advmss {
assert!(json.contains("\"advmss\""));
}
let deserialized: Route = serde_json::from_str(&json).unwrap();
assert_eq!(route, deserialized);
}
#[rstest]
#[case(
r#"{
"cniVersion": "1.0.0",
"name": "test",
"type": "bridge",
"ipMasq": true
}"#,
Some(true)
)]
#[case(
r#"{
"cniVersion": "1.0.0",
"name": "test",
"type": "bridge",
"ipMasq": false
}"#,
Some(false)
)]
#[case(
r#"{
"cniVersion": "1.0.0",
"name": "test",
"type": "bridge"
}"#,
None
)]
fn test_net_conf_with_ip_masq(#[case] input: &str, #[case] expected: Option<bool>) {
let conf: NetConf = serde_json::from_str(input).unwrap();
assert_eq!(conf.ip_masq, expected);
let json = serde_json::to_string(&conf).unwrap();
if expected.is_some() {
assert!(json.contains("ipMasq"));
} else {
assert!(!json.contains("ipMasq"));
}
}
#[rstest]
#[case(
Dns {
nameservers: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()],
domain: Some("example.com".to_string()),
search: Some(vec![
"example.com".to_string(),
"corp.example.com".to_string(),
]),
options: Some(vec!["ndots:5".to_string(), "timeout:1".to_string()]),
},
true,
true,
true
)]
#[case(
Dns {
nameservers: vec!["1.1.1.1".to_string()],
domain: None,
search: None,
options: None,
},
false,
false,
false
)]
#[case(
Dns {
nameservers: vec!["10.0.0.1".to_string()],
domain: Some("local".to_string()),
search: None,
options: Some(vec!["ndots:1".to_string()]),
},
true,
false,
true
)]
fn test_dns_all_fields(
#[case] dns: Dns,
#[case] has_domain: bool,
#[case] has_search: bool,
#[case] has_options: bool,
) {
let json = serde_json::to_string(&dns).unwrap();
let deserialized: Dns = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.nameservers, dns.nameservers);
assert_eq!(deserialized.domain, dns.domain);
assert_eq!(deserialized.search, dns.search);
assert_eq!(deserialized.options, dns.options);
assert_eq!(deserialized.domain.is_some(), has_domain);
assert_eq!(deserialized.search.is_some(), has_search);
assert_eq!(deserialized.options.is_some(), has_options);
}
}