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}