containerd_shimkit/sandbox/
cli.rs

1//! Command line interface for the containerd shim.
2//!
3//! The CLI provides the interface between containerd and the Wasm runtime.
4//! It handles commands like start and delete from containerd's shim API.
5//!
6//! ## Usage
7//!
8//! The shim binary should be named `containerd-shim-<engine>-v1` and installed in $PATH.
9//! containerd will call the shim with various commands.
10//!
11//! ## Configuration
12//!
13//! The shim can be configured using the [`Config`] struct:
14//!
15//! ```rust,no_run
16//! use containerd_shimkit::Config;
17//!
18//! let config = Config {
19//!     // Disable automatic logger setup
20//!     no_setup_logger: false,
21//!     // Set default log level
22//!     default_log_level: "info".to_string(),
23//!     // Disable child process reaping
24//!     no_reaper: false,
25//!     // Disable subreaper setting
26//!     no_sub_reaper: false,
27//! };
28//! ```
29//!
30//! ## Version Information
31//!
32//! The module provides two macros for version information:
33//!
34//! - [`version!()`] - Returns the crate version from Cargo.toml
35//! - [`revision!()`] - Returns the Git revision hash, if available
36//!
37//! ## Example usage:
38//!
39//! ```rust,no_run
40//! use containerd_shimkit::{
41//!     shim_version,
42//!     sandbox::{Instance, InstanceConfig, Result},
43//!     sandbox::cli::shim_main,
44//!     sandbox::sync::WaitableCell,
45//!     Config,
46//! };
47//! use tokio::time::sleep;
48//! use chrono::{DateTime, Utc};
49//! use std::time::Duration;
50//!
51//! #[derive(Clone, Default)]
52//! struct MyInstance {
53//!     exit_code: WaitableCell<(u32, DateTime<Utc>)>,
54//! };
55//!
56//! impl Instance for MyInstance {
57//!     async fn new(id: String, cfg: &InstanceConfig) -> Result<Self> {
58//!         let exit_code = WaitableCell::new();
59//!         Ok(Self { exit_code })
60//!     }
61//!     async fn start(&self) -> Result<u32> {
62//!         let exit_code = self.exit_code.clone();
63//!         tokio::spawn(async move {
64//!             sleep(Duration::from_millis(100)).await;
65//!             let _ = exit_code.set((0, Utc::now()));
66//!         });
67//!         Ok(42) // some id for our task, usually a PID
68//!     }
69//!     async fn kill(&self, signal: u32) -> Result<()> {
70//!         Ok(()) // no-op
71//!     }
72//!     async fn delete(&self) -> Result<()> {
73//!         Ok(()) // no-op
74//!     }
75//!     async fn wait(&self) -> (u32, DateTime<Utc>) {
76//!         *self.exit_code.wait().await
77//!     }
78//! }
79//!
80//! let config = Config {
81//!     default_log_level: "error".to_string(),
82//!     ..Default::default()
83//! };
84//!
85//! shim_main::<MyInstance>(
86//!     "my-engine",
87//!     shim_version!(),
88//!     Some(config),
89//! );
90//! ```
91//!
92//! When the `opentelemetry` feature is enabled, additional runtime config
93//! is available through environment variables:
94//!
95//! - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`: Enable OpenTelemetry tracing
96//! - `OTEL_EXPORTER_OTLP_ENDPOINT`: Enable OpenTelemetry tracing as above
97//! - `OTEL_SDK_DISABLED`: Disable OpenTelemetry SDK
98//!
99
100use std::path::PathBuf;
101
102use containerd_shim::{Config, parse, run};
103
104#[cfg(feature = "opentelemetry")]
105use crate::sandbox::async_utils::AmbientRuntime as _;
106#[cfg(feature = "opentelemetry")]
107use crate::sandbox::shim::{OtlpConfig, otel_traces_enabled};
108use crate::sandbox::{Instance, Shim};
109
110pub mod r#impl {
111    pub use git_version::git_version;
112}
113
114pub use crate::{revision, version};
115
116/// Get the crate version from Cargo.toml.
117#[macro_export]
118macro_rules! version {
119    () => {
120        option_env!("CARGO_PKG_VERSION")
121    };
122}
123
124/// Get the Git revision hash, if available.
125#[macro_export]
126macro_rules! revision {
127    () => {
128        match $crate::sandbox::cli::r#impl::git_version!(
129            args = ["--match=:", "--always", "--abbrev=15", "--dirty=.m"],
130            fallback = "",
131        ) {
132            "" => None,
133            version => Some(version),
134        }
135    };
136}
137
138pub struct Version {
139    pub version: &'static str,
140    pub revision: &'static str,
141}
142
143#[macro_export]
144macro_rules! shim_version {
145    () => {
146        $crate::sandbox::cli::Version {
147            version: $crate::version!().unwrap_or("<none>"),
148            revision: $crate::revision!().unwrap_or("<none>"),
149        }
150    };
151}
152
153impl Default for Version {
154    fn default() -> Self {
155        Self {
156            version: "<none>",
157            revision: "<none>",
158        }
159    }
160}
161
162#[cfg(target_os = "linux")]
163fn get_mem(pid: u32) -> (usize, usize) {
164    let mut rss = 0;
165    let mut total = 0;
166    for line in std::fs::read_to_string(format!("/proc/{pid}/status"))
167        .unwrap()
168        .lines()
169    {
170        let line = line.trim();
171        // VmPeak is the maximum total virtual memory used so far.
172        // VmHWM (high water mark) is the maximum resident set memory used so far.
173        // See: https://man7.org/linux/man-pages/man5/proc_pid_status.5.html
174        if let Some(rest) = line.strip_prefix("VmPeak:") {
175            if let Some(rest) = rest.strip_suffix("kB") {
176                total = rest.trim().parse().unwrap_or(0);
177            }
178        } else if let Some(rest) = line.strip_prefix("VmHWM:") {
179            if let Some(rest) = rest.strip_suffix("kB") {
180                rss = rest.trim().parse().unwrap_or(0);
181            }
182        }
183    }
184    (rss, total)
185}
186
187#[cfg(target_os = "linux")]
188fn log_mem() {
189    let pid = std::process::id();
190    let (rss, tot) = get_mem(pid);
191    log::info!("Shim peak memory usage was: peak resident set {rss} kB, peak total {tot} kB");
192
193    let pid = zygote::Zygote::global().run(|_| std::process::id(), ());
194    let (rss, tot) = get_mem(pid);
195    log::info!("Zygote peak memory usage was: peak resident set {rss} kB, peak total {tot} kB");
196}
197
198#[cfg(unix)]
199fn init_zygote_and_logger(debug: bool, config: &Config) {
200    zygote::Zygote::init();
201    if config.no_setup_logger {
202        return;
203    }
204    zygote::Zygote::global().run(
205        |(debug, default_log_level)| {
206            // last two arguments are unused in unix
207            crate::vendor::containerd_shim::logger::init(debug, &default_log_level, "", "")
208                .expect("Failed to initialize logger");
209        },
210        (debug, config.default_log_level.clone()),
211    );
212}
213
214/// Main entry point for the shim.
215///
216/// If the `opentelemetry` feature is enabled, this function will start the shim with OpenTelemetry tracing.
217///
218/// It parses OTLP configuration from the environment and initializes the OpenTelemetry SDK.
219pub fn shim_main<I>(name: &str, version: Version, config: Option<Config>)
220where
221    I: 'static + Instance + Sync + Send,
222{
223    // parse the version flag
224    let os_args: Vec<_> = std::env::args_os().collect();
225
226    let flags = parse(&os_args[1..]).unwrap();
227    let argv0 = PathBuf::from(&os_args[0]);
228    let argv0 = argv0.file_stem().unwrap_or_default().to_string_lossy();
229
230    if flags.version {
231        println!("{argv0}:");
232        println!("  Runtime: {name}");
233        println!("  Version: {}", version.version);
234        println!("  Revision: {}", version.revision);
235        println!();
236
237        std::process::exit(0);
238    }
239
240    // Initialize the zygote and logger for the container process
241    #[cfg(unix)]
242    {
243        let default_config = Config::default();
244        let config = config.as_ref().unwrap_or(&default_config);
245        init_zygote_and_logger(flags.debug, config);
246    }
247
248    #[cfg(feature = "opentelemetry")]
249    if otel_traces_enabled() {
250        // opentelemetry uses tokio, so we need to initialize a runtime
251        async {
252            let otlp_config = OtlpConfig::build_from_env().expect("Failed to build OtelConfig.");
253            let _guard = otlp_config
254                .init()
255                .expect("Failed to initialize OpenTelemetry.");
256            tokio::task::block_in_place(move || {
257                shim_main_inner::<I>(name, config);
258            });
259        }
260        .block_on();
261    } else {
262        shim_main_inner::<I>(name, config);
263    }
264
265    #[cfg(not(feature = "opentelemetry"))]
266    {
267        shim_main_inner::<I>(name, config);
268    }
269
270    #[cfg(target_os = "linux")]
271    log_mem();
272}
273
274#[cfg_attr(feature = "tracing", tracing::instrument(level = "Info"))]
275fn shim_main_inner<I>(name: &str, config: Option<Config>)
276where
277    I: 'static + Instance + Sync + Send,
278{
279    #[cfg(feature = "opentelemetry")]
280    {
281        // read TRACECONTEXT env var that's set by the parent process
282        if let Ok(ctx) = std::env::var("TRACECONTEXT") {
283            OtlpConfig::set_trace_context(&ctx).unwrap();
284        } else {
285            let ctx = OtlpConfig::get_trace_context().unwrap();
286            // SAFETY: although it's in a multithreaded context,
287            // it's safe to assume that all the other threads are not
288            // messing with env vars at this point.
289            unsafe {
290                std::env::set_var("TRACECONTEXT", ctx);
291            }
292        }
293    }
294
295    run::<Shim<I>>(name, config);
296}