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_shim_wasm::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_shim_wasm::{
41//!     container::{Instance, Engine, RuntimeContext},
42//!     sandbox::cli::{revision, shim_main, version},
43//!     Config,
44//! };
45//! use anyhow::Result;
46//!
47//! #[derive(Clone, Default)]
48//! struct MyEngine;
49//!
50//! impl Engine for MyEngine {
51//!     fn name() -> &'static str {
52//!         "my-engine"
53//!     }
54//!
55//!     fn run_wasi(&self, ctx: &impl RuntimeContext) -> Result<i32> {
56//!         Ok(0)
57//!     }
58//! }
59//!
60//! let config = Config {
61//!     default_log_level: "error".to_string(),
62//!     ..Default::default()
63//! };
64//!
65//! shim_main::<Instance<MyEngine>>(
66//!     "my-engine",
67//!     version!(),
68//!     revision!(),
69//!     "v1",
70//!     Some(config),
71//! );
72//! ```
73//!
74//! When the `opentelemetry` feature is enabled, additional runtime config
75//! is available through environment variables:
76//!
77//! - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`: Enable OpenTelemetry tracing
78//! - `OTEL_EXPORTER_OTLP_ENDPOINT`: Enable OpenTelemetry tracing as above
79//! - `OTEL_SDK_DISABLED`: Disable OpenTelemetry SDK
80//!
81
82use std::path::PathBuf;
83
84use containerd_shim::{Config, parse, run};
85
86#[cfg(feature = "opentelemetry")]
87use crate::sandbox::async_utils::AmbientRuntime as _;
88#[cfg(feature = "opentelemetry")]
89use crate::sandbox::shim::{OtlpConfig, otel_traces_enabled};
90use crate::sandbox::{Instance, Shim};
91
92pub mod r#impl {
93    pub use git_version::git_version;
94}
95
96pub use crate::{revision, version};
97
98/// Get the crate version from Cargo.toml.
99#[macro_export]
100macro_rules! version {
101    () => {
102        env!("CARGO_PKG_VERSION")
103    };
104}
105
106/// Get the Git revision hash, if available.
107#[macro_export]
108macro_rules! revision {
109    () => {
110        match $crate::sandbox::cli::r#impl::git_version!(
111            args = ["--match=:", "--always", "--abbrev=15", "--dirty=.m"],
112            fallback = "",
113        ) {
114            "" => None,
115            version => Some(version),
116        }
117    };
118}
119
120#[cfg(target_os = "linux")]
121fn get_mem(pid: u32) -> (usize, usize) {
122    let mut rss = 0;
123    let mut total = 0;
124    for line in std::fs::read_to_string(format!("/proc/{pid}/status"))
125        .unwrap()
126        .lines()
127    {
128        let line = line.trim();
129        // VmPeak is the maximum total virtual memory used so far.
130        // VmHWM (high water mark) is the maximum resident set memory used so far.
131        // See: https://man7.org/linux/man-pages/man5/proc_pid_status.5.html
132        if let Some(rest) = line.strip_prefix("VmPeak:") {
133            if let Some(rest) = rest.strip_suffix("kB") {
134                total = rest.trim().parse().unwrap_or(0);
135            }
136        } else if let Some(rest) = line.strip_prefix("VmHWM:") {
137            if let Some(rest) = rest.strip_suffix("kB") {
138                rss = rest.trim().parse().unwrap_or(0);
139            }
140        }
141    }
142    (rss, total)
143}
144
145#[cfg(target_os = "linux")]
146fn log_mem() {
147    let pid = std::process::id();
148    let (rss, tot) = get_mem(pid);
149    log::info!("Shim peak memory usage was: peak resident set {rss} kB, peak total {tot} kB");
150
151    let pid = zygote::Zygote::global().run(|_| std::process::id(), ());
152    let (rss, tot) = get_mem(pid);
153    log::info!("Zygote peak memory usage was: peak resident set {rss} kB, peak total {tot} kB");
154}
155
156#[cfg(unix)]
157fn init_zygote_and_logger(debug: bool, config: &Config) {
158    zygote::Zygote::init();
159    if config.no_setup_logger {
160        return;
161    }
162    zygote::Zygote::global().run(
163        |(debug, default_log_level)| {
164            // last two arguments are unused in unix
165            crate::vendor::containerd_shim::logger::init(debug, &default_log_level, "", "")
166                .expect("Failed to initialize logger");
167        },
168        (debug, config.default_log_level.clone()),
169    );
170}
171
172/// Main entry point for the shim.
173///
174/// If the `opentelemetry` feature is enabled, this function will start the shim with OpenTelemetry tracing.
175///
176/// It parses OTLP configuration from the environment and initializes the OpenTelemetry SDK.
177pub fn shim_main<'a, I>(
178    name: &str,
179    version: &str,
180    revision: impl Into<Option<&'a str>> + std::fmt::Debug,
181    shim_version: impl Into<Option<&'a str>> + std::fmt::Debug,
182    config: Option<Config>,
183) where
184    I: 'static + Instance + Sync + Send,
185{
186    // parse the version flag
187    let os_args: Vec<_> = std::env::args_os().collect();
188
189    let flags = parse(&os_args[1..]).unwrap();
190    let argv0 = PathBuf::from(&os_args[0]);
191    let argv0 = argv0.file_stem().unwrap_or_default().to_string_lossy();
192
193    if flags.version {
194        println!("{argv0}:");
195        println!("  Runtime: {name}");
196        println!("  Version: {version}");
197        println!("  Revision: {}", revision.into().unwrap_or("<none>"));
198        println!();
199
200        std::process::exit(0);
201    }
202
203    // Initialize the zygote and logger for the container process
204    #[cfg(unix)]
205    {
206        let default_config = Config::default();
207        let config = config.as_ref().unwrap_or(&default_config);
208        init_zygote_and_logger(flags.debug, config);
209    }
210
211    #[cfg(feature = "opentelemetry")]
212    if otel_traces_enabled() {
213        // opentelemetry uses tokio, so we need to initialize a runtime
214        async {
215            let otlp_config = OtlpConfig::build_from_env().expect("Failed to build OtelConfig.");
216            let _guard = otlp_config
217                .init()
218                .expect("Failed to initialize OpenTelemetry.");
219            tokio::task::block_in_place(move || {
220                shim_main_inner::<I>(name, shim_version, config);
221            });
222        }
223        .block_on();
224    } else {
225        shim_main_inner::<I>(name, shim_version, config);
226    }
227
228    #[cfg(not(feature = "opentelemetry"))]
229    {
230        shim_main_inner::<I>(name, shim_version, config);
231    }
232
233    #[cfg(target_os = "linux")]
234    log_mem();
235}
236
237#[cfg_attr(feature = "tracing", tracing::instrument(level = "Info"))]
238fn shim_main_inner<'a, I>(
239    name: &str,
240    shim_version: impl Into<Option<&'a str>> + std::fmt::Debug,
241    config: Option<Config>,
242) where
243    I: 'static + Instance + Sync + Send,
244{
245    #[cfg(feature = "opentelemetry")]
246    {
247        // read TRACECONTEXT env var that's set by the parent process
248        if let Ok(ctx) = std::env::var("TRACECONTEXT") {
249            OtlpConfig::set_trace_context(&ctx).unwrap();
250        } else {
251            let ctx = OtlpConfig::get_trace_context().unwrap();
252            // SAFETY: although it's in a multithreaded context,
253            // it's safe to assume that all the other threads are not
254            // messing with env vars at this point.
255            unsafe {
256                std::env::set_var("TRACECONTEXT", ctx);
257            }
258        }
259    }
260
261    let shim_version = shim_version.into().unwrap_or("v1");
262    let lower_name = name.to_lowercase();
263    let shim_id = format!("io.containerd.{lower_name}.{shim_version}");
264
265    run::<Shim<I>>(&shim_id, config);
266}