ci: Introduce tests for VM template factory

Add k8s-vm-templating-test.bats which exercises pod create
with the factory initialized on the target node.

Signed-off-by: Cameron Baird <cameronbaird@microsoft.com>
This commit is contained in:
Cameron Baird
2026-06-09 18:55:28 +00:00
parent c0f9744225
commit 65a5f272f8
12 changed files with 402 additions and 38 deletions

View File

@@ -41,10 +41,12 @@ jobs:
matrix:
environment: [
{ vmm: clh, containerd_version: latest },
{ vmm: clh, containerd_version: latest, snapshotter: erofs, erofs_mode: disk, erofs_merge_mode: unmerged },
{ vmm: clh, containerd_version: minimum },
{ vmm: dragonball, containerd_version: latest },
{ vmm: dragonball, containerd_version: minimum },
{ vmm: qemu, containerd_version: latest },
{ vmm: qemu, containerd_version: latest, snapshotter: erofs, erofs_mode: disk, erofs_merge_mode: unmerged },
{ vmm: qemu, containerd_version: minimum },
{ vmm: qemu-runtime-rs, containerd_version: latest },
{ vmm: qemu-runtime-rs, containerd_version: minimum },
@@ -68,6 +70,9 @@ jobs:
K8S_TEST_HOST_TYPE: baremetal-no-attestation
CONTAINER_ENGINE: containerd
CONTAINER_ENGINE_VERSION: ${{ matrix.environment.containerd_version }}
SNAPSHOTTER: ${{ matrix.environment.snapshotter }}
EROFS_SNAPSHOTTER_MODE: ${{ matrix.environment.erofs_mode }}
EROFS_MERGE_MODE: ${{ matrix.environment.erofs_merge_mode }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

View File

@@ -822,7 +822,7 @@ func (clh *cloudHypervisor) shouldRestoreFromTemplate() bool {
return true
}
// copyFile copies a file from src to dst
// copyFile copies a file from src to dst, preserving the source file's permissions.
func (clh *cloudHypervisor) copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
@@ -830,7 +830,12 @@ func (clh *cloudHypervisor) copyFile(src, dst string) error {
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
srcInfo, err := srcFile.Stat()
if err != nil {
return err
}
dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, srcInfo.Mode())
if err != nil {
return err
}
@@ -880,7 +885,7 @@ func (clh *cloudHypervisor) updateVsockSocketPath(configPath, vmID string) error
return err
}
return os.WriteFile(configPath, updatedConfig, 0644)
return os.WriteFile(configPath, updatedConfig, 0600)
}
// setupInitdata prepares and attaches the initdata disk if present.
@@ -1578,7 +1583,7 @@ func (clh *cloudHypervisor) SaveVM() error {
return err
}
if err := os.WriteFile(snapshotConfigPath, modifiedConfig, 0644); err != nil {
if err := os.WriteFile(snapshotConfigPath, modifiedConfig, 0600); err != nil {
clh.Logger().WithError(err).Error("Failed to write modified snapshot config")
return err
}

View File

@@ -457,7 +457,8 @@ func TestCloudHypervisorCleanupVM(t *testing.T) {
assert.NoError(err, "persist.GetDriver() unexpected error")
dir := filepath.Join(store.RunVMStoragePath(), clh.id)
os.MkdirAll(dir, os.ModePerm)
err = os.MkdirAll(dir, os.ModePerm)
assert.NoError(err, "failed to create dir %s", dir)
err = clh.cleanupVM(false)
assert.NoError(err, "persist.GetDriver() unexpected error")
@@ -566,7 +567,8 @@ func TestClhRestoreVM(t *testing.T) {
assert.Contains(err.Error(), filepath.Join(clhConfig.VMStorePath, "state.json"))
// Now create the VM snapshot files and call restoreVM again.
os.MkdirAll(clhConfig.VMStorePath, os.ModePerm)
err = os.MkdirAll(clhConfig.VMStorePath, os.ModePerm)
assert.NoError(err, "failed to create dir %s", clhConfig.VMStorePath)
stateFile := filepath.Join(clhConfig.VMStorePath, "state.json")
configFile := filepath.Join(clhConfig.VMStorePath, "config.json")
err = os.WriteFile(stateFile, []byte("{}"), 0o600)

View File

@@ -82,7 +82,6 @@ func resetHypervisorConfig(config *vc.VMConfig) {
config.HypervisorConfig.RunStorePath = ""
config.HypervisorConfig.SandboxName = ""
config.HypervisorConfig.SandboxNamespace = ""
config.HypervisorConfig.DefaultMaxVCPUs = 0
}
// It's important that baseConfig and newConfig are passed by value!

View File

@@ -43,6 +43,7 @@ TEST_CLUSTER_NAMESPACE="${TEST_CLUSTER_NAMESPACE:-}"
CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-containerd}"
SNAPSHOTTER="${SNAPSHOTTER:-}"
EROFS_SNAPSHOTTER_MODE="${EROFS_SNAPSHOTTER_MODE:-}"
EROFS_MERGE_MODE="${EROFS_MERGE_MODE:-}"
# Wait for the Kubernetes API to recover after kata-deploy uninstall, then
# retry the uninstall to purge any stale helm release state. On k3s/rke2,
@@ -851,6 +852,26 @@ function helm_helper() {
yq -i '.containerd.userDropIn = strenv(HELM_CONTAINERD_USER_DROP_IN)' "${values_yaml}"
fi
# EROFS merge mode ("merged" default, or "unmerged"). This is orthogonal
# to EROFS_SNAPSHOTTER_MODE (which controls default_size): it controls
# whether containerd merges layers into a single fsmeta.erofs (merged,
# runtime-rs only) or keeps per-layer layer.erofs (unmerged, required by
# the Go runtime).
if [[ -n "${EROFS_MERGE_MODE}" ]]; then
if [[ "${SNAPSHOTTER}" != "erofs" ]]; then
die "EROFS_MERGE_MODE is only supported with SNAPSHOTTER=erofs"
fi
case "${EROFS_MERGE_MODE}" in
merged|unmerged) ;;
*)
die "Unsupported EROFS_MERGE_MODE: ${EROFS_MERGE_MODE}"
;;
esac
yq -i ".snapshotter.erofsMergeMode = \"${EROFS_MERGE_MODE}\"" "${values_yaml}"
fi
if [[ -z "${HELM_SHIMS}" ]]; then
die "A list of shims is expected but none was provided"
fi

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env bats
#
# Copyright (c) 2024 Kata Containers
#
# SPDX-License-Identifier: Apache-2.0
#
# Tests for Kata VM templating (factory) functionality in Kubernetes integration mode
load "${BATS_TEST_DIRNAME}/lib.sh"
load "${BATS_TEST_DIRNAME}/../../common.bash"
load "${BATS_TEST_DIRNAME}/confidential_common.sh"
load "${BATS_TEST_DIRNAME}/tests_common.sh"
# Returns 0 if the current environment supports VM templating, non-zero
# otherwise. VM templating is only supported on non-confidential clh/qemu
# hypervisors, and because it uses shared_fs="none" it also requires a
# block-device-based snapshotter (blockfile or erofs).
vm_templating_supported() {
[[ "${KATA_HYPERVISOR}" == "clh" || "${KATA_HYPERVISOR}" == "qemu" ]] || return 1
is_confidential_runtime_class && return 1
[[ "${SNAPSHOTTER:-}" =~ ^(blockfile|erofs)$ ]] || return 1
return 0
}
setup() {
if ! vm_templating_supported; then
skip "VM templating requires a non-confidential clh/qemu hypervisor and a blockfile/erofs snapshotter (KATA_HYPERVISOR=${KATA_HYPERVISOR}, SNAPSHOTTER=${SNAPSHOTTER:-unset})"
fi
setup_common || die "setup_common failed"
# Build a Kata runtime config drop-in that enables VM templating and
# disables shared_fs (incompatible with templating).
# QEMU VM templating requires an initrd, CLH does not.
local rootfs_override=""
if [[ "${KATA_HYPERVISOR}" == "qemu" ]]; then
rootfs_override=$'image = ""\ninitrd = "/opt/kata/share/kata-containers/kata-containers-initrd.img"'
fi
local runtime_config_dropin_file="${BATS_TEST_TMPDIR}/99-k8s-vm-templating.toml"
cat > "${runtime_config_dropin_file}" <<DROPIN
[hypervisor.${KATA_HYPERVISOR}]
shared_fs = "none"
default_vcpus = 1
default_memory = 512
${rootfs_override}
[factory]
enable_template = true
template_path = "/run/vc/vm/template"
DROPIN
# Install the drop-in on the node selected by setup_common and record the
# remote path so teardown can remove it.
dropin_path="$(set_kata_runtime_config_dropin_file "$node" "${runtime_config_dropin_file}")" \
|| die "Failed to install Kata runtime config drop-in on node $node"
# kata-runtime defaults to the QEMU config; point it at the active
# hypervisor so that factory init/destroy use the correct configuration.
kata_config_path="/opt/kata/share/defaults/kata-containers/runtimes/${KATA_HYPERVISOR}/configuration-${KATA_HYPERVISOR}.toml"
}
@test "Pod can be created with a templated VM" {
# Initialize the VM template on the target node.
exec_host "$node" "nsenter --mount=/proc/1/ns/mnt /opt/kata/bin/kata-runtime --config ${kata_config_path} factory init"
# The factory init above must have created the template directory. exec_host
# pipes the remote output through `tr`, so the pipeline's exit status is not
# the remote command's; assert on the output instead. Check inside PID 1's
# mount namespace, where the template tmpfs was actually mounted.
exec_host "$node" "nsenter --mount=/proc/1/ns/mnt test -f /run/vc/vm/template/memory && echo present" | grep -q present
pod_name="test-templated-pod"
ctr_name="test-container"
pod_config=$(mktemp --tmpdir pod_config.XXXXXX.yaml)
cp "$pod_config_dir/busybox-template.yaml" "$pod_config"
sed -i "s/POD_NAME/$pod_name/" "$pod_config"
sed -i "s/CTR_NAME/$ctr_name/" "$pod_config"
kubectl create -f "${pod_config}"
kubectl wait --for=condition=Ready --timeout="$timeout" "pod/${pod_name}"
grep_pod_exec_output "${pod_name}" "Hello from templated VM" sh -c "echo 'Hello from templated VM'"
# Confirm at least one VM sandbox under /run/vc/vm/ is a symlink, which
# proves the factory/template path was used. A non-templated VM creates a
# real directory at /run/vc/vm/<sandbox-id>/, whereas a factory-spawned VM
# stores its state under a generated UUID and /run/vc/vm/<sandbox-id> is a
# symlink pointing at it (see assignSandbox() in
# src/runtime/virtcontainers/vm.go). Inspect PID 1's mount namespace, where
# the shim creates these entries alongside the template tmpfs.
exec_host "$node" \
"nsenter --mount=/proc/1/ns/mnt find /run/vc/vm -maxdepth 1 -mindepth 1 -type l ! -name template | grep -q . && echo symlink" \
| grep -q symlink
}
teardown() {
vm_templating_supported || return 0
rm -f "${pod_config:-}"
# Destroy the VM template and remove the config drop-in on the target node.
# factory destroy must run in PID 1's mount namespace to unmount the template
# tmpfs that factory init created there (see the @test for details).
exec_host "$node" "nsenter --mount=/proc/1/ns/mnt /opt/kata/bin/kata-runtime --config ${kata_config_path} factory destroy" \
|| echo "Warning: Failed to destroy VM template on node $node"
remove_kata_runtime_config_dropin_file "$node" "${dropin_path:-}" \
|| echo "Warning: Failed to remove Kata runtime config drop-in on node $node"
teardown_common "${node:-}" "${node_start_time:-}"
}

View File

@@ -104,6 +104,7 @@ else
"k8s-security-context.bats" \
"k8s-shared-volume.bats" \
"k8s-volume.bats" \
"k8s-vm-templating.bats" \
"k8s-nginx-connectivity.bats" \
)

View File

@@ -15,36 +15,50 @@ use std::path::Path;
pub async fn configure_erofs_snapshotter(config: &Config, configuration_file: &Path) -> Result<()> {
info!("Configuring erofs-snapshotter");
// "unmerged" mode keeps each image layer as its own per-layer `layer.erofs`
// (containerd's default, non-fsmerged layout), which is the only layout the
// Go runtime can consume. In the default "merged" mode we force containerd
// to merge layers into a single `fsmeta.erofs`, which is runtime-rs only.
let unmerged = config.erofs_merge_mode.as_deref() == Some("unmerged");
// The Go runtime does not support fsmerged EROFS (fsmeta.erofs).
// If the snapshotter handler mapping explicitly pairs a Go shim with
// erofs, that is a hard misconfiguration — bail out so the operator
// fixes the mapping instead of hitting cryptic runtime errors later.
if let Some(mapping) = config.snapshotter_handler_mapping_for_arch.as_ref() {
let mut go_shims_on_erofs = Vec::new();
for entry in mapping.split(',') {
let parts: Vec<&str> = entry.split(':').collect();
if parts.len() == 2 && parts[1] == "erofs" && !utils::is_rust_shim(parts[0]) {
go_shims_on_erofs.push(parts[0].to_string());
// erofs in the (default) merged mode, that is a hard misconfiguration —
// bail out so the operator fixes the mapping instead of hitting cryptic
// runtime errors later. In "unmerged" mode the Go runtime is supported, so
// skip this guard.
if !unmerged {
if let Some(mapping) = config.snapshotter_handler_mapping_for_arch.as_ref() {
let mut go_shims_on_erofs = Vec::new();
for entry in mapping.split(',') {
let parts: Vec<&str> = entry.split(':').collect();
if parts.len() == 2 && parts[1] == "erofs" && !utils::is_rust_shim(parts[0]) {
go_shims_on_erofs.push(parts[0].to_string());
}
}
}
if !go_shims_on_erofs.is_empty() {
warn!("##########################################################################");
warn!("# #");
warn!("# Go runtime shim(s) mapped to the erofs snapshotter: #");
for s in &go_shims_on_erofs {
warn!("# - {:<64} #", s);
if !go_shims_on_erofs.is_empty() {
warn!("##########################################################################");
warn!("# #");
warn!("# Go runtime shim(s) mapped to the erofs snapshotter: #");
for s in &go_shims_on_erofs {
warn!("# - {:<64} #", s);
}
warn!("# #");
warn!(
"# The Go runtime does NOT support fsmerged EROFS (fsmeta.erofs). #"
);
warn!("# Only runtime-rs shims are supported with merged erofs. Set #");
warn!("# EROFS_MERGE_MODE=unmerged to use the Go runtime with erofs. #");
warn!("# #");
warn!("##########################################################################");
return Err(anyhow::anyhow!(
"erofs snapshotter: Go runtime shim(s) [{}] cannot be mapped to merged erofs. \
The Go runtime does not support fsmerged EROFS. \
Set EROFS_MERGE_MODE=unmerged, remove these shims from \
SNAPSHOTTER_HANDLER_MAPPING, or switch them to runtime-rs.",
go_shims_on_erofs.join(", ")
));
}
warn!("# #");
warn!("# The Go runtime does NOT support fsmerged EROFS (fsmeta.erofs). #");
warn!("# Only runtime-rs shims are supported with the erofs snapshotter. #");
warn!("# #");
warn!("##########################################################################");
return Err(anyhow::anyhow!(
"erofs snapshotter: Go runtime shim(s) [{}] cannot be mapped to erofs. \
The Go runtime does not support fsmerged EROFS. \
Remove these shims from SNAPSHOTTER_HANDLER_MAPPING or switch them to runtime-rs.",
go_shims_on_erofs.join(", ")
));
}
}
@@ -88,11 +102,27 @@ pub async fn configure_erofs_snapshotter(config: &Config, configuration_file: &P
".plugins.\"io.containerd.snapshotter.v1.erofs\".default_size",
"\"10G\"",
)?;
toml_utils::set_toml_value(
configuration_file,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
"0",
)?;
// In the default "merged" mode, force containerd to merge all layers into a
// single fsmeta.erofs (max_unmerged_layers = 0). In "unmerged" mode we delete
// any previously-written value so each layer stays a separate layer.erofs,
// which the Go runtime requires.
//
// Because kata-deploy edits the containerd config in place, switching from
// merged to unmerged must actively remove the old `max_unmerged_layers = 0`
// left behind by a previous install. Otherwise the stale `0` would keep
// forcing the merged layout and break Go-runtime compatibility.
if !unmerged {
toml_utils::set_toml_value(
configuration_file,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
"0",
)?;
} else {
toml_utils::delete_toml_value(
configuration_file,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
)?;
}
Ok(())
}

View File

@@ -178,6 +178,14 @@ pub struct Config {
pub multi_install_suffix: Option<String>,
pub helm_post_delete_hook: bool,
pub experimental_setup_snapshotter: Option<Vec<String>>,
/// EROFS snapshotter merge mode: "merged" (default) or "unmerged".
///
/// In "unmerged" mode kata-deploy does not force containerd's erofs
/// snapshotter to merge layers (it leaves `max_unmerged_layers` at the
/// containerd default), so each image layer is exposed as its own
/// per-layer `layer.erofs`. This is the only layout the Go runtime can
/// consume; the merged (`fsmeta.erofs`) layout is runtime-rs only.
pub erofs_merge_mode: Option<String>,
pub experimental_force_guest_pull_for_arch: Vec<String>,
pub dest_dir: String,
pub host_install_dir: String,
@@ -307,6 +315,11 @@ impl Config {
.filter(|s| !s.is_empty())
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
let erofs_merge_mode = env::var("EROFS_MERGE_MODE")
.ok()
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty());
// Only use arch-specific variable for experimental force guest pull
let experimental_force_guest_pull_for_arch =
get_arch_var("EXPERIMENTAL_FORCE_GUEST_PULL", "", &arch)
@@ -338,6 +351,7 @@ impl Config {
multi_install_suffix,
helm_post_delete_hook,
experimental_setup_snapshotter,
erofs_merge_mode,
experimental_force_guest_pull_for_arch,
dest_dir,
host_install_dir,
@@ -508,6 +522,17 @@ impl Config {
_ => {}
}
// Validate EROFS_MERGE_MODE
// Only "merged" (default) and "unmerged" are accepted.
if let Some(mode) = self.erofs_merge_mode.as_ref() {
if mode != "merged" && mode != "unmerged" {
return Err(anyhow::anyhow!(
"EROFS_MERGE_MODE must be either 'merged' or 'unmerged', got '{}'",
mode
));
}
}
// Validate EXPERIMENTAL_FORCE_GUEST_PULL_FOR_ARCH
// This is a list of shim names
for shim in &self.experimental_force_guest_pull_for_arch {
@@ -551,6 +576,7 @@ impl Config {
"* EXPERIMENTAL_SETUP_SNAPSHOTTER: {:?}",
self.experimental_setup_snapshotter
);
info!("* EROFS_MERGE_MODE: {:?}", self.erofs_merge_mode);
info!(
"* EXPERIMENTAL_FORCE_GUEST_PULL: {}",
self.experimental_force_guest_pull_for_arch.join(",")

View File

@@ -121,6 +121,47 @@ pub fn set_toml_value(file_path: &Path, path: &str, value: &str) -> Result<()> {
Ok(())
}
/// Delete a TOML value (or table) at a given path.
///
/// Navigates to the parent table and removes the final key. This is a no-op if
/// any path component (including the final key) does not exist, so callers can
/// unconditionally remove a value that may or may not be present.
pub fn delete_toml_value(file_path: &Path, path: &str) -> Result<()> {
let content = std::fs::read_to_string(file_path)
.with_context(|| format!("Failed to read TOML file: {file_path:?}"))?;
let (header, toml_content) = split_non_toml_header(&content);
let mut doc = toml_content
.parse::<DocumentMut>()
.context("Failed to parse TOML")?;
let parts = parse_toml_path(path)?;
let mut current_table = doc.as_table_mut();
for (i, part) in parts.iter().enumerate() {
let is_last = i == parts.len() - 1;
if is_last {
// Remove the value; absent key is fine (no-op).
current_table.remove(part.as_str());
} else {
// Navigate into the intermediate table. If it does not exist, there
// is nothing to delete.
match current_table
.get_mut(part.as_str())
.and_then(|item| item.as_table_mut())
{
Some(table) => current_table = table,
None => return Ok(()),
}
}
}
write_toml_with_header(file_path, header, &doc)?;
Ok(())
}
/// Get a TOML value at a given path
pub fn get_toml_value(file_path: &Path, path: &str) -> Result<String> {
let content = std::fs::read_to_string(file_path)
@@ -1714,4 +1755,100 @@ imports = ["/etc/containerd/conf.d/*.toml", "/opt/kata/containerd/config.d/kata-
.unwrap();
assert_eq!(runtime_type, "io.containerd.kata-qemu.v2");
}
#[test]
fn test_delete_toml_value() {
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path();
std::fs::write(
temp_path,
"[plugins.\"io.containerd.snapshotter.v1.erofs\"]\nmax_unmerged_layers = 0\nenable_fsverity = true\n",
)
.unwrap();
// Sanity check: value is present before deletion.
let before = get_toml_value(
temp_path,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
)
.unwrap();
assert_eq!(before, "0");
delete_toml_value(
temp_path,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
)
.unwrap();
// The deleted key is gone, but sibling keys remain.
let result = get_toml_value(
temp_path,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
);
assert!(result.is_err(), "deleted key should no longer be found");
let sibling = get_toml_value(
temp_path,
".plugins.\"io.containerd.snapshotter.v1.erofs\".enable_fsverity",
)
.unwrap();
assert_eq!(sibling, "true", "sibling keys must be preserved");
}
#[test]
fn test_delete_toml_value_missing_key_is_noop() {
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path();
let initial = "[plugins.\"io.containerd.snapshotter.v1.erofs\"]\nenable_fsverity = true\n";
std::fs::write(temp_path, initial).unwrap();
// Deleting a key that does not exist must succeed and leave the file usable.
delete_toml_value(
temp_path,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
)
.unwrap();
// Deleting through a non-existent intermediate table is also a no-op.
delete_toml_value(temp_path, ".plugins.\"nonexistent.plugin\".some_key").unwrap();
let sibling = get_toml_value(
temp_path,
".plugins.\"io.containerd.snapshotter.v1.erofs\".enable_fsverity",
)
.unwrap();
assert_eq!(sibling, "true");
}
#[test]
fn test_delete_toml_value_preserves_k3s_header() {
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path();
std::fs::write(
temp_path,
"{{ template \"base\" . }}\n[plugins.\"io.containerd.snapshotter.v1.erofs\"]\nmax_unmerged_layers = 0\n",
)
.unwrap();
delete_toml_value(
temp_path,
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
)
.unwrap();
let content = std::fs::read_to_string(temp_path).unwrap();
assert!(
content.starts_with("{{ template \"base\" . }}\n"),
"non-TOML header must be preserved"
);
assert!(
!content.contains("max_unmerged_layers"),
"value must be removed"
);
}
#[test]
fn test_delete_toml_value_nonexistent_file() {
let result = delete_toml_value(Path::new("/nonexistent/file.toml"), "some.path");
assert!(result.is_err());
}
}

View File

@@ -413,6 +413,13 @@ Get snapshotter setup list from structured config
{{- join "," .Values.snapshotter.setup -}}
{{- end -}}
{{/*
Get EROFS merge mode from structured config ("merged" or "unmerged")
*/}}
{{- define "kata-deploy.getErofsMergeMode" -}}
{{- .Values.snapshotter.erofsMergeMode | default "" -}}
{{- end -}}
{{/*
Get debug value from structured config
*/}}
@@ -569,6 +576,11 @@ e.g. `{{- include "kata-deploy.commonEnv" . | nindent 8 }}`.
- name: EXPERIMENTAL_SETUP_SNAPSHOTTER
value: {{ $snapshotterSetup | quote }}
{{- end }}
{{- $erofsMergeMode := include "kata-deploy.getErofsMergeMode" . | trim -}}
{{- if $erofsMergeMode }}
- name: EROFS_MERGE_MODE
value: {{ $erofsMergeMode | quote }}
{{- end }}
{{- $forceGuestPullAmd64 := include "kata-deploy.getForceGuestPullForArch" (dict "root" . "arch" "amd64") | trim -}}
{{- if $forceGuestPullAmd64 }}
- name: EXPERIMENTAL_FORCE_GUEST_PULL_X86_64

View File

@@ -271,6 +271,18 @@ health:
snapshotter:
setup: ["nydus"] # ["nydus", "erofs"] or []
# EROFS merge mode: "merged" (default) or "unmerged".
#
# "merged" forces containerd's erofs snapshotter to merge all image layers
# into a single fsmeta.erofs (max_unmerged_layers = 0). This layout is only
# supported by runtime-rs shims.
#
# "unmerged" leaves max_unmerged_layers at the containerd default so each
# image layer is exposed as its own per-layer layer.erofs. This is the only
# layout the Go runtime can consume, so set this when mapping a Go shim to the
# erofs snapshotter. When empty, kata-deploy uses its built-in default
# (merged).
erofsMergeMode: ""
# Shim configuration
# By default (disableAll: false), all shims with enabled: ~ (null) are enabled.