Merge pull request #12937 from fidencio/topic/kata-deploy-support-containerd-config-version-4

kata-deploy: support containerd config version 4
This commit is contained in:
Fabiano Fidêncio
2026-05-01 07:46:36 +02:00
committed by GitHub
5 changed files with 206 additions and 28 deletions

View File

@@ -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)?;

View File

@@ -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<bool> {
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",

View File

@@ -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<u32> {
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<u32>,
runtime: &str,
) -> Option<u32> {
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<u32> {
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<u32> {
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<u32>) -> &'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, &params)?;
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, &params)?;
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
);

View File

@@ -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<u32> {
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::<u32>() {
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<u32>) {
assert_eq!(major_version_from_config_toml(content), expected);
}
}

View File

@@ -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::*;