diff --git a/tools/packaging/kata-deploy/binary/src/artifacts/snapshotters.rs b/tools/packaging/kata-deploy/binary/src/artifacts/snapshotters.rs index f647ed92d0..c9ae3bb52f 100644 --- a/tools/packaging/kata-deploy/binary/src/artifacts/snapshotters.rs +++ b/tools/packaging/kata-deploy/binary/src/artifacts/snapshotters.rs @@ -142,7 +142,7 @@ pub async fn configure_snapshotter( // Runtime plugin id (from paths or by reading config), then map to table where disable_snapshot_annotations lives. let runtime_plugin_id = match &paths.plugin_id { Some(id) => id.as_str(), - None => containerd::get_containerd_pluginid(&paths.config_file)?, + None => containerd::get_containerd_pluginid(&paths.config_file, runtime)?, }; let pluginid = containerd::pluginid_for_snapshotter_annotations(runtime_plugin_id, &paths.config_file)?; diff --git a/tools/packaging/kata-deploy/binary/src/config.rs b/tools/packaging/kata-deploy/binary/src/config.rs index 55583c7477..3e4f73f5bf 100644 --- a/tools/packaging/kata-deploy/binary/src/config.rs +++ b/tools/packaging/kata-deploy/binary/src/config.rs @@ -12,7 +12,8 @@ use std::path::Path; use crate::k8s; /// K3s/RKE2 containerd config template filenames (under the mounted containerd dir). -/// V3 is for containerd 2.x; V2 is for containerd 1.x. +/// `config-v3.toml.tmpl` is used when the rendered config uses split-CRI schema (containerd config version >= 3, including 4+). +/// `config.toml.tmpl` is for legacy CRI (version 2). pub const K3S_RKE2_CONTAINERD_V3_TMPL: &str = "/etc/containerd/config-v3.toml.tmpl"; pub const K3S_RKE2_CONTAINERD_V2_TMPL: &str = "/etc/containerd/config.toml.tmpl"; @@ -21,8 +22,8 @@ pub const K3S_RKE2_CONTAINERD_V2_TMPL: &str = "/etc/containerd/config.toml.tmpl" /// snapshotter field, and the base name for the data directory and socket path on the host. pub const NYDUS_FOR_KATA_TEE: &str = "nydus-for-kata-tee"; -/// Resolves whether to use containerd config v3 (true) or v2 (false) for K3s/RKE2. -/// 1. Tries config.toml (containerd config file): if it exists and contains "version = 3" or "version = 2", use that. +/// Resolves whether to use the containerd 2.x split-CRI layout (true) or the v1 CRI gRPC layout (false) for K3s/RKE2. +/// 1. Tries config.toml: if it has `version = 2` use legacy CRI table; if `version >= 3` (including 4+) use split CRI. /// 2. Else falls back to the node's containerRuntimeVersion (e.g. "containerd://2.1.5-k3s1"). /// 3. If neither is available, returns an error. pub fn k3s_rke2_resolve_use_v3( @@ -30,14 +31,17 @@ pub fn k3s_rke2_resolve_use_v3( container_runtime_version: Option<&str>, ) -> Result { use crate::runtime::manager; + use crate::utils::major_version_from_config_toml; // 1. Try config.toml (generated config that may already exist on the node) if let Ok(content) = fs::read_to_string(config_file_path) { - if content.contains("version = 3") { - return Ok(true); - } - if content.contains("version = 2") { - return Ok(false); + if let Some(v) = major_version_from_config_toml(&content) { + if v == 2 { + return Ok(false); + } + if v >= 3 { + return Ok(true); + } } } @@ -48,8 +52,8 @@ pub fn k3s_rke2_resolve_use_v3( // 3. Neither source available Err(anyhow::anyhow!( - "K3s/RKE2: cannot determine containerd config version (v2 vs v3). \ - Need version from {config_file_path} (version = 2/3) or node containerRuntimeVersion." + "K3s/RKE2: cannot determine containerd config version (v2 vs split-CRI). \ + Need version from {config_file_path} (version = 2 or >= 3) or node containerRuntimeVersion." )) } @@ -911,6 +915,15 @@ mod tests { ); } + #[serial] + #[test] + fn test_k3s_rke2_resolve_use_v3_from_config_version_4_without_node_version() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "version = 4\n").unwrap(); + assert!(k3s_rke2_resolve_use_v3(path.to_str().unwrap(), None).unwrap()); + } + #[rstest] #[case( "imports = [\"/var/lib/rancher/k3s/agent/etc/containerd/config.toml.d/*.toml\"]\n", diff --git a/tools/packaging/kata-deploy/binary/src/runtime/containerd.rs b/tools/packaging/kata-deploy/binary/src/runtime/containerd.rs index 6f8cd875ec..87333e55fa 100644 --- a/tools/packaging/kata-deploy/binary/src/runtime/containerd.rs +++ b/tools/packaging/kata-deploy/binary/src/runtime/containerd.rs @@ -38,26 +38,83 @@ const CONTAINERD_CRI_IMAGES_PLUGIN_ID: &str = "\"io.containerd.cri.v1.images\""; /// Plugin table for CRI containerd in v2 (disable_snapshot_annotations lives here). const CONTAINERD_CRI_CONTAINERD_TABLE_V2: &str = "\"io.containerd.grpc.v1.cri\".containerd"; -/// Reads config and returns the CRI plugin ID used for *runtime* config (runtimes, snapshotter-per-runtime). -pub(crate) fn get_containerd_pluginid(config_file: &str) -> Result<&'static str> { - let content = fs::read_to_string(config_file) - .with_context(|| format!("Failed to read containerd config file: {}", config_file))?; +fn is_k3s_or_rke2(runtime: &str) -> bool { + matches!(runtime, "k3s" | "k3s-agent" | "rke2-agent" | "rke2-server") +} - if content.contains("version = 3") { - Ok(CONTAINERD_V3_RUNTIME_PLUGIN_ID) - } else if content.contains("version = 2") { - Ok(CONTAINERD_V2_CRI_PLUGIN_ID) - } else { - Ok(CONTAINERD_LEGACY_CRI_PLUGIN_ID) +fn schema_version_from_k3s_rke2_rendered_config() -> Option { + fs::read_to_string(crate::config::k3s_rke2_rendered_config_path()) + .ok() + .and_then(|c| utils::major_version_from_config_toml(&c)) +} + +/// If `primary_schema` is unset, try the rendered K3s/RKE2 `config.toml`. +/// In strict `get_containerd_pluginid` parsing, this only applies when the primary config is +/// readable but has no root `version`; missing-file fallback only happens in lenient readers. +fn schema_version_with_k3s_rke2_fallback( + primary_schema: Option, + runtime: &str, +) -> Option { + primary_schema.or_else(|| { + if is_k3s_or_rke2(runtime) { + schema_version_from_k3s_rke2_rendered_config() + } else { + None + } + }) +} + +/// Root config schema `version = N` using lenient reads. +/// +/// Reads `primary` via `fs::read_to_string`; on failure (missing path, permissions, etc.) +/// the parsed schema is treated as unset. If that result has no root `version`, or the read +/// failed, falls back to the rendered K3s/RKE2 `/etc/containerd/config.toml` when `runtime` +/// is k3s/rke2 (covers templates without `version`, transient mounts, and similar). +fn schema_version_relaxed(primary: &str, runtime: &str) -> Option { + let primary_v = fs::read_to_string(primary) + .ok() + .and_then(|c| utils::major_version_from_config_toml(&c)); + schema_version_with_k3s_rke2_fallback(primary_v, runtime) +} + +fn containerd_config_schema_version(paths: &ContainerdPaths, runtime: &str) -> Option { + schema_version_relaxed(&paths.config_file, runtime) +} + +/// TOML path for containerd log level when DEBUG=true. Config schema v4+ uses +/// `plugins."io.containerd.server.v1.debug"` instead of deprecated top-level `[debug]`. +fn containerd_debug_level_toml_path(config_schema_version: Option) -> &'static str { + match config_schema_version { + Some(v) if v >= 4 => concat!(".plugins.", "\"io.containerd.server.v1.debug\"", ".level"), + _ => ".debug.level", } } -/// True when the containerd config is v3 (version = 3), i.e. we use the split CRI plugins. +/// Reads config and returns the CRI plugin ID used for *runtime* config (runtimes, snapshotter-per-runtime). +/// `runtime` selects K3s/RKE2 fallbacks when `config_file` is a template without `version`. +pub(crate) fn get_containerd_pluginid(config_file: &str, runtime: &str) -> Result<&'static str> { + let content = fs::read_to_string(config_file) + .with_context(|| format!("Failed to read containerd config file: {}", config_file))?; + + let v = schema_version_with_k3s_rke2_fallback( + utils::major_version_from_config_toml(&content), + runtime, + ); + + match v { + Some(ver) if ver >= 3 => Ok(CONTAINERD_V3_RUNTIME_PLUGIN_ID), + Some(2) => Ok(CONTAINERD_V2_CRI_PLUGIN_ID), + _ => Ok(CONTAINERD_LEGACY_CRI_PLUGIN_ID), + } +} + +/// True when the containerd config uses split CRI plugins (`io.containerd.cri.v1.*`), +/// i.e. config schema version >= 3 (including containerd's newer defaults such as version 4). fn is_containerd_v3_config(pluginid: &str) -> bool { pluginid == CONTAINERD_V3_RUNTIME_PLUGIN_ID } -/// Maps the runtime plugin ID (from get_containerd_pluginid) to the plugin table where +/// Maps the runtime plugin ID (from `get_containerd_pluginid` / K3s `paths.plugin_id`) to the table where /// disable_snapshot_annotations lives. In v3 that's the *images* plugin; in v2 the CRI .containerd subtable. pub(crate) fn pluginid_for_snapshotter_annotations( runtime_plugin_id: &str, @@ -69,7 +126,7 @@ pub(crate) fn pluginid_for_snapshotter_annotations( Ok(CONTAINERD_CRI_CONTAINERD_TABLE_V2) } else { anyhow::bail!( - "Containerd config {} has no \"version = 2\" or \"version = 3\"; cannot determine CRI plugin for snapshotter config", + "Containerd config {} has no supported config schema (need version = 2 or version >= 3); cannot determine CRI plugin for snapshotter config", config_file ) } @@ -179,7 +236,7 @@ pub async fn configure_containerd_runtime( let configuration_file = get_containerd_output_path(&paths); let pluginid = match paths.plugin_id.as_deref() { Some(plugin_id) => plugin_id, - None => get_containerd_pluginid(&paths.config_file)?, + None => get_containerd_pluginid(&paths.config_file, runtime)?, }; log::info!( @@ -236,7 +293,9 @@ pub async fn configure_containerd_runtime( write_containerd_runtime_config(&configuration_file, pluginid, ¶ms)?; if config.debug { - toml_utils::set_toml_value(&configuration_file, ".debug.level", "\"debug\"")?; + let schema = containerd_config_schema_version(&paths, runtime); + let debug_path = containerd_debug_level_toml_path(schema); + toml_utils::set_toml_value(&configuration_file, debug_path, "\"debug\"")?; } Ok(()) @@ -257,7 +316,7 @@ pub async fn configure_custom_containerd_runtime( let configuration_file = get_containerd_output_path(&paths); let pluginid = match paths.plugin_id.as_deref() { Some(plugin_id) => plugin_id, - None => get_containerd_pluginid(&paths.config_file)?, + None => get_containerd_pluginid(&paths.config_file, runtime)?, }; log::info!( @@ -298,6 +357,12 @@ pub async fn configure_custom_containerd_runtime( write_containerd_runtime_config(&configuration_file, pluginid, ¶ms)?; + if config.debug { + let schema = containerd_config_schema_version(&paths, runtime); + let debug_path = containerd_debug_level_toml_path(schema); + toml_utils::set_toml_value(&configuration_file, debug_path, "\"debug\"")?; + } + Ok(()) } @@ -627,6 +692,40 @@ mod tests { } } + #[test] + fn test_containerd_debug_level_toml_path_by_schema_version() { + assert_eq!( + containerd_debug_level_toml_path(Some(4)), + ".plugins.\"io.containerd.server.v1.debug\".level" + ); + assert_eq!( + containerd_debug_level_toml_path(Some(5)), + ".plugins.\"io.containerd.server.v1.debug\".level" + ); + assert_eq!(containerd_debug_level_toml_path(Some(3)), ".debug.level"); + assert_eq!(containerd_debug_level_toml_path(None), ".debug.level"); + } + + #[test] + fn test_get_containerd_pluginid_version_4_uses_split_cri() { + let f = NamedTempFile::new().unwrap(); + std::fs::write(f.path(), "version = 4\n").unwrap(); + assert_eq!( + get_containerd_pluginid(f.path().to_str().unwrap(), "containerd").unwrap(), + CONTAINERD_V3_RUNTIME_PLUGIN_ID + ); + } + + #[test] + fn test_get_containerd_pluginid_version_2() { + let f = NamedTempFile::new().unwrap(); + std::fs::write(f.path(), "version = 2\n").unwrap(); + assert_eq!( + get_containerd_pluginid(f.path().to_str().unwrap(), "containerd").unwrap(), + CONTAINERD_V2_CRI_PLUGIN_ID + ); + } + /// CRI images runtime_platforms snapshotter is set only for v3 config when a snapshotter is configured. #[rstest] #[case(CONTAINERD_V3_RUNTIME_PLUGIN_ID, Some("\"nydus\""), "kata-qemu", true)] @@ -699,7 +798,7 @@ mod tests { err ); assert!( - err.to_string().contains("version = 2") || err.to_string().contains("version = 3"), + err.to_string().contains("version = 2") || err.to_string().contains("version >= 3"), "error should mention version: {}", err ); diff --git a/tools/packaging/kata-deploy/binary/src/utils/containerd_config_version.rs b/tools/packaging/kata-deploy/binary/src/utils/containerd_config_version.rs new file mode 100644 index 0000000000..6f04738bc4 --- /dev/null +++ b/tools/packaging/kata-deploy/binary/src/utils/containerd_config_version.rs @@ -0,0 +1,64 @@ +// Copyright (c) 2026 Kata Containers community +// +// SPDX-License-Identifier: Apache-2.0 + +//! Helpers for reading `version = N` from containerd `config.toml`. + +/// Reads the schema `version = N` from the containerd config root table only (before any `[` TOML table header). +/// +/// Containerd keeps `version` as a top-level key; keys under `[plugins]` or other tables are ignored. +/// Ignores `#` comments on the same line. Malformed `version` lines are skipped so a later valid line can still match. +/// Returns `None` if no valid root `version` key is found. +pub fn major_version_from_config_toml(content: &str) -> Option { + for raw_line in content.lines() { + let line = raw_line.split('#').next().unwrap_or("").trim(); + if line.is_empty() { + continue; + } + if line.starts_with('[') { + break; + } + let mut parts = line.splitn(2, '='); + let key = parts.next().unwrap_or("").trim(); + if key != "version" { + continue; + } + let Some(rhs) = parts.next() else { + continue; + }; + let value = rhs.trim(); + let num_str: String = value.chars().take_while(|c| c.is_ascii_digit()).collect(); + if num_str.is_empty() { + continue; + } + if let Ok(n) = num_str.parse::() { + return Some(n); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("version = 4\n", Some(4))] + #[case("version=3\n", Some(3))] + #[case(" version = 2 \n", Some(2))] + #[case("version = 4 # comment\n", Some(4))] + // Other root keys may precede `version` + #[case("root = '/foo'\nversion = 3\n", Some(3))] + // Only root table: ignore `version` under [plugins] + #[case("version = 2\n\n[plugins]\n version = 999\n", Some(2))] + #[case("[plugins]\nversion = 3\n", None)] + // Malformed lines are skipped until a valid `version = N` + #[case("version = abc\nversion = 4\n", Some(4))] + #[case("version\nversion = 3\n", Some(3))] + #[case("root = '/foo'\n", None)] + #[case("", None)] + fn test_major_version_from_config_toml(#[case] content: &str, #[case] expected: Option) { + assert_eq!(major_version_from_config_toml(content), expected); + } +} diff --git a/tools/packaging/kata-deploy/binary/src/utils/mod.rs b/tools/packaging/kata-deploy/binary/src/utils/mod.rs index a8fc054c2d..556b559135 100644 --- a/tools/packaging/kata-deploy/binary/src/utils/mod.rs +++ b/tools/packaging/kata-deploy/binary/src/utils/mod.rs @@ -3,8 +3,10 @@ // // SPDX-License-Identifier: Apache-2.0 +pub mod containerd_config_version; pub mod system; pub mod toml; pub mod yaml; +pub use containerd_config_version::major_version_from_config_toml; pub use system::*;