rust_cni/libcni/
exec.rs

1// Copyright (c) 2024 https://github.com/divinerapier/cni-rs
2use log::{debug, error, trace, warn};
3use serde::{Deserialize, Serialize};
4
5use crate::libcni::result::ResultCNI;
6use crate::libcni::CNIError;
7use std::path::Path;
8use std::process::{Command, Stdio};
9use std::{collections::HashMap, io::Write};
10
11#[derive(Default, Serialize, Deserialize, Debug)]
12pub struct ExecArgs {
13    pub(crate) command: String,
14    pub(crate) containerd_id: String,
15    pub(crate) netns: String,
16    pub(crate) plugin_args: Vec<[String; 2]>,
17    pub(crate) plugin_args_str: String,
18    pub(crate) ifname: String,
19    pub(crate) path: String,
20}
21
22impl ExecArgs {
23    pub fn to_env(&self) -> Vec<String> {
24        debug!("Preparing environment for CNI execution , args :{:?}", self);
25        let mut result_env = Vec::default();
26
27        // Set environment variables
28        std::env::set_var("CNI_COMMAND", self.command.clone());
29        std::env::set_var("CNI_CONTAINERID", self.containerd_id.clone());
30        std::env::set_var("CNI_NETNS", self.netns.clone());
31        std::env::set_var("CNI_ARGS", self.plugin_args_str.clone());
32        std::env::set_var("CNI_IFNAME", self.ifname.clone());
33        std::env::set_var("CNI_PATH", self.path.clone());
34
35        // Collect all environment variables
36        for (k, v) in std::env::vars() {
37            result_env.push(format!("{}={}", k, v));
38        }
39
40        trace!(
41            "CNI environment prepared with {} variables",
42            result_env.len()
43        );
44        result_env
45    }
46}
47
48pub trait Exec {
49    fn exec_plugins(
50        &self,
51        plugin_path: String,
52        stdin_data: &[u8],
53        environ: Vec<String>,
54    ) -> super::ResultCNI<Vec<u8>>;
55
56    fn find_in_path(&self, plugin: String, paths: Vec<String>) -> ResultCNI<String>;
57
58    fn decode(&self, data: &[u8]) -> ResultCNI<()>;
59}
60
61#[derive(Default)]
62pub struct RawExec {}
63
64impl Exec for RawExec {
65    fn exec_plugins(
66        &self,
67        plugin_path: String,
68        stdin_data: &[u8],
69        environ: Vec<String>,
70    ) -> ResultCNI<Vec<u8>> {
71        debug!("Executing CNI plugin: {}", plugin_path);
72        trace!("CNI stdin data: {}", String::from_utf8_lossy(stdin_data));
73
74        // Parse environment variables
75        let envs: HashMap<String, String> = environ
76            .iter()
77            .filter_map(|env_var| {
78                let parts: Vec<&str> = env_var.splitn(2, '=').collect();
79                if parts.len() == 2 {
80                    Some((parts[0].to_string(), parts[1].to_string()))
81                } else {
82                    None
83                }
84            })
85            .collect();
86        // debug!("CNI environment variables: {:?}", envs);
87        // Check if plugin exists
88        if !Path::new(&plugin_path).exists() {
89            let err_msg = format!("CNI plugin not found: {}", plugin_path);
90            return Err(Box::new(CNIError::ExecuteError(err_msg)));
91        }
92
93        // Start the plugin process
94        let mut plugin_cmd = match Command::new(&plugin_path)
95            .stdin(Stdio::piped())
96            .stdout(Stdio::piped())
97            .stderr(Stdio::piped())
98            .envs(envs)
99            .spawn()
100        {
101            Ok(cmd) => cmd,
102            Err(e) => {
103                let err_msg = format!("Failed to start CNI plugin {}: {}", plugin_path, e);
104                return Err(Box::new(CNIError::ExecuteError(err_msg)));
105            }
106        };
107        debug!("cni stdin is: {:?}", String::from_utf8_lossy(stdin_data));
108        // Write stdin data
109        if let Some(mut stdin) = plugin_cmd.stdin.take() {
110            if let Err(e) = stdin.write_all(stdin_data) {
111                let err_msg = format!("Failed to write to plugin stdin: {}", e);
112                return Err(Box::new(CNIError::ExecuteError(err_msg)));
113            }
114            // Close stdin to signal end of input
115            drop(stdin);
116        }
117
118        // Wait for command to complete and get output
119        let output = match plugin_cmd.wait_with_output() {
120            Ok(output) => output,
121            Err(e) => {
122                let err_msg = format!("Failed to get plugin output: {}", e);
123                return Err(Box::new(CNIError::ExecuteError(err_msg)));
124            }
125        };
126
127        // Check for errors in stderr
128        if !output.stderr.is_empty() {
129            let stderr = String::from_utf8_lossy(&output.stderr);
130            warn!("CNI plugin stderr: {}", stderr);
131        }
132
133        // Check for error in stdout (CNI returns errors in JSON format)
134        if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&output.stdout) {
135            if let Some(error_code) = json_value.get("code") {
136                if error_code.as_u64().is_some() {
137                    let msg = String::from_utf8_lossy(&output.stdout).to_string();
138                    return Err(Box::new(CNIError::ExecuteError(msg)));
139                }
140            }
141        }
142
143        debug!("CNI plugin execution successful");
144        Ok(output.stdout)
145    }
146
147    fn find_in_path(&self, plugin: String, paths: Vec<String>) -> ResultCNI<String> {
148        trace!("Finding CNI plugin {} in paths", plugin);
149
150        if paths.is_empty() {
151            let err_msg = format!("No plugin paths provided for {}", plugin);
152            error!("{}", err_msg);
153            return Err(Box::new(CNIError::Config(err_msg)));
154        }
155
156        for path in &paths {
157            let full_path = format!("{}/{}", path, plugin);
158            let plugin_path = Path::new(&full_path);
159
160            if plugin_path.exists() {
161                debug!("Found CNI plugin at: {}", full_path);
162                return Ok(full_path);
163            }
164        }
165
166        let err_msg = format!("CNI plugin {} not found in paths {:?}", plugin, paths);
167        error!("{}", err_msg);
168        Err(Box::new(CNIError::NotFound(plugin, paths.join(":"))))
169    }
170
171    fn decode(&self, data: &[u8]) -> ResultCNI<()> {
172        trace!("Decoding CNI data: {} bytes", data.len());
173
174        // Simple validation of JSON structure
175        match serde_json::from_slice::<serde_json::Value>(data) {
176            Ok(_) => {
177                trace!("CNI data successfully decoded");
178                Ok(())
179            }
180            Err(e) => {
181                let err_msg = format!("Failed to decode CNI data: {}", e);
182                error!("{}", err_msg);
183                Err(Box::new(CNIError::VarDecode(
184                    "Invalid JSON format".to_string(),
185                )))
186            }
187        }
188    }
189}