Skip to content

Commit 73489ea

Browse files
committed
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>
1 parent fb64b20 commit 73489ea

12 files changed

Lines changed: 392 additions & 38 deletions

File tree

.github/workflows/run-k8s-tests-on-free-runner.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ jobs:
4141
matrix:
4242
environment: [
4343
{ vmm: clh, containerd_version: latest },
44+
{ vmm: clh, containerd_version: latest, snapshotter: erofs, erofs_mode: disk, erofs_merge_mode: unmerged },
4445
{ vmm: clh, containerd_version: minimum },
4546
{ vmm: dragonball, containerd_version: latest },
4647
{ vmm: dragonball, containerd_version: minimum },
4748
{ vmm: qemu, containerd_version: latest },
49+
{ vmm: qemu, containerd_version: latest, snapshotter: erofs, erofs_mode: disk, erofs_merge_mode: unmerged },
4850
{ vmm: qemu, containerd_version: minimum },
4951
{ vmm: qemu-runtime-rs, containerd_version: latest },
5052
{ vmm: qemu-runtime-rs, containerd_version: minimum },
@@ -68,6 +70,9 @@ jobs:
6870
K8S_TEST_HOST_TYPE: baremetal-no-attestation
6971
CONTAINER_ENGINE: containerd
7072
CONTAINER_ENGINE_VERSION: ${{ matrix.environment.containerd_version }}
73+
SNAPSHOTTER: ${{ matrix.environment.snapshotter }}
74+
EROFS_SNAPSHOTTER_MODE: ${{ matrix.environment.erofs_mode }}
75+
EROFS_MERGE_MODE: ${{ matrix.environment.erofs_merge_mode }}
7176
GH_TOKEN: ${{ github.token }}
7277
steps:
7378
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

src/runtime/virtcontainers/clh.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -816,15 +816,20 @@ func (clh *cloudHypervisor) shouldRestoreFromTemplate() bool {
816816
return true
817817
}
818818

819-
// copyFile copies a file from src to dst
819+
// copyFile copies a file from src to dst, preserving the source file's permissions.
820820
func (clh *cloudHypervisor) copyFile(src, dst string) error {
821821
srcFile, err := os.Open(src)
822822
if err != nil {
823823
return err
824824
}
825825
defer srcFile.Close()
826826

827-
dstFile, err := os.Create(dst)
827+
srcInfo, err := srcFile.Stat()
828+
if err != nil {
829+
return err
830+
}
831+
832+
dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, srcInfo.Mode())
828833
if err != nil {
829834
return err
830835
}
@@ -874,7 +879,7 @@ func (clh *cloudHypervisor) updateVsockSocketPath(configPath, vmID string) error
874879
return err
875880
}
876881

877-
return os.WriteFile(configPath, updatedConfig, 0644)
882+
return os.WriteFile(configPath, updatedConfig, 0600)
878883
}
879884

880885
// setupInitdata prepares and attaches the initdata disk if present.
@@ -1572,7 +1577,7 @@ func (clh *cloudHypervisor) SaveVM() error {
15721577
return err
15731578
}
15741579

1575-
if err := os.WriteFile(snapshotConfigPath, modifiedConfig, 0644); err != nil {
1580+
if err := os.WriteFile(snapshotConfigPath, modifiedConfig, 0600); err != nil {
15761581
clh.Logger().WithError(err).Error("Failed to write modified snapshot config")
15771582
return err
15781583
}

src/runtime/virtcontainers/clh_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,8 @@ func TestCloudHypervisorCleanupVM(t *testing.T) {
457457
assert.NoError(err, "persist.GetDriver() unexpected error")
458458

459459
dir := filepath.Join(store.RunVMStoragePath(), clh.id)
460-
os.MkdirAll(dir, os.ModePerm)
460+
err = os.MkdirAll(dir, os.ModePerm)
461+
assert.NoError(err, "failed to create dir %s", dir)
461462

462463
err = clh.cleanupVM(false)
463464
assert.NoError(err, "persist.GetDriver() unexpected error")
@@ -566,7 +567,8 @@ func TestClhRestoreVM(t *testing.T) {
566567
assert.Contains(err.Error(), filepath.Join(clhConfig.VMStorePath, "state.json"))
567568

568569
// Now create the VM snapshot files and call restoreVM again.
569-
os.MkdirAll(clhConfig.VMStorePath, os.ModePerm)
570+
err = os.MkdirAll(clhConfig.VMStorePath, os.ModePerm)
571+
assert.NoError(err, "failed to create dir %s", clhConfig.VMStorePath)
570572
stateFile := filepath.Join(clhConfig.VMStorePath, "state.json")
571573
configFile := filepath.Join(clhConfig.VMStorePath, "config.json")
572574
err = os.WriteFile(stateFile, []byte("{}"), 0o600)

src/runtime/virtcontainers/factory/factory_linux.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ func resetHypervisorConfig(config *vc.VMConfig) {
8282
config.HypervisorConfig.RunStorePath = ""
8383
config.HypervisorConfig.SandboxName = ""
8484
config.HypervisorConfig.SandboxNamespace = ""
85-
config.HypervisorConfig.DefaultMaxVCPUs = 0
8685
}
8786

8887
// It's important that baseConfig and newConfig are passed by value!

tests/gha-run-k8s-common.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ TEST_CLUSTER_NAMESPACE="${TEST_CLUSTER_NAMESPACE:-}"
4343
CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-containerd}"
4444
SNAPSHOTTER="${SNAPSHOTTER:-}"
4545
EROFS_SNAPSHOTTER_MODE="${EROFS_SNAPSHOTTER_MODE:-}"
46+
EROFS_MERGE_MODE="${EROFS_MERGE_MODE:-}"
4647

4748
# Wait for the Kubernetes API to recover after kata-deploy uninstall, then
4849
# retry the uninstall to purge any stale helm release state. On k3s/rke2,
@@ -851,6 +852,26 @@ function helm_helper() {
851852
yq -i '.containerd.userDropIn = strenv(HELM_CONTAINERD_USER_DROP_IN)' "${values_yaml}"
852853
fi
853854

855+
# EROFS merge mode ("merged" default, or "unmerged"). This is orthogonal
856+
# to EROFS_SNAPSHOTTER_MODE (which controls default_size): it controls
857+
# whether containerd merges layers into a single fsmeta.erofs (merged,
858+
# runtime-rs only) or keeps per-layer layer.erofs (unmerged, required by
859+
# the Go runtime).
860+
if [[ -n "${EROFS_MERGE_MODE}" ]]; then
861+
if [[ "${SNAPSHOTTER}" != "erofs" ]]; then
862+
die "EROFS_MERGE_MODE is only supported with SNAPSHOTTER=erofs"
863+
fi
864+
865+
case "${EROFS_MERGE_MODE}" in
866+
merged|unmerged) ;;
867+
*)
868+
die "Unsupported EROFS_MERGE_MODE: ${EROFS_MERGE_MODE}"
869+
;;
870+
esac
871+
872+
yq -i ".snapshotter.erofsMergeMode = \"${EROFS_MERGE_MODE}\"" "${values_yaml}"
873+
fi
874+
854875
if [[ -z "${HELM_SHIMS}" ]]; then
855876
die "A list of shims is expected but none was provided"
856877
fi
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env bats
2+
#
3+
# Copyright (c) 2024 Kata Containers
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
#
7+
# Tests for Kata VM templating (factory) functionality in Kubernetes integration mode
8+
9+
load "${BATS_TEST_DIRNAME}/lib.sh"
10+
load "${BATS_TEST_DIRNAME}/../../common.bash"
11+
load "${BATS_TEST_DIRNAME}/confidential_common.sh"
12+
load "${BATS_TEST_DIRNAME}/tests_common.sh"
13+
14+
# Returns 0 if the current environment supports VM templating, non-zero
15+
# otherwise. VM templating is only supported on non-confidential clh/qemu
16+
# hypervisors, and because it uses shared_fs="none" it also requires a
17+
# block-device-based snapshotter (blockfile or erofs).
18+
vm_templating_supported() {
19+
[[ "${KATA_HYPERVISOR}" == "clh" || "${KATA_HYPERVISOR}" == "qemu" ]] || return 1
20+
is_confidential_runtime_class && return 1
21+
[[ "${SNAPSHOTTER:-}" =~ ^(blockfile|erofs)$ ]] || return 1
22+
return 0
23+
}
24+
25+
setup() {
26+
if ! vm_templating_supported; then
27+
skip "VM templating requires a non-confidential clh/qemu hypervisor and a blockfile/erofs snapshotter (KATA_HYPERVISOR=${KATA_HYPERVISOR}, SNAPSHOTTER=${SNAPSHOTTER:-unset})"
28+
fi
29+
30+
setup_common || die "setup_common failed"
31+
32+
# Build a Kata runtime config drop-in that enables VM templating and
33+
# disables shared_fs (incompatible with templating).
34+
local runtime_config_dropin_file="${BATS_TEST_TMPDIR}/99-k8s-vm-templating.toml"
35+
cat > "${runtime_config_dropin_file}" <<DROPIN
36+
[hypervisor.${KATA_HYPERVISOR}]
37+
shared_fs = "none"
38+
default_vcpus = 1
39+
default_memory = 512
40+
41+
[factory]
42+
enable_template = true
43+
template_path = "/run/vc/vm/template"
44+
DROPIN
45+
46+
# Install the drop-in on the node selected by setup_common and record the
47+
# remote path so teardown can remove it.
48+
dropin_path="$(set_kata_runtime_config_dropin_file "$node" "${runtime_config_dropin_file}")" \
49+
|| die "Failed to install Kata runtime config drop-in on node $node"
50+
}
51+
52+
@test "Pod can be created with a templated VM" {
53+
# Initialize the VM template on the target node. Use the absolute path:
54+
# kata-deploy installs kata-runtime under /opt/kata/bin, which is not on the
55+
# node's sudo PATH.
56+
exec_host "$node" "sudo /opt/kata/bin/kata-runtime factory init"
57+
58+
# The factory init above must have created the template directory. exec_host
59+
# pipes the remote output through `tr`, so the pipeline's exit status is not
60+
# the remote command's; assert on the output instead.
61+
exec_host "$node" "test -d /run/vc/vm/template && echo present" | grep -q present
62+
63+
pod_name="test-templated-pod"
64+
ctr_name="test-container"
65+
66+
pod_config=$(mktemp --tmpdir pod_config.XXXXXX.yaml)
67+
cp "$pod_config_dir/busybox-template.yaml" "$pod_config"
68+
69+
sed -i "s/POD_NAME/$pod_name/" "$pod_config"
70+
sed -i "s/CTR_NAME/$ctr_name/" "$pod_config"
71+
72+
kubectl create -f "${pod_config}"
73+
kubectl wait --for=condition=Ready --timeout="$timeout" "pod/${pod_name}"
74+
75+
grep_pod_exec_output "${pod_name}" "Hello from templated VM" sh -c "echo 'Hello from templated VM'"
76+
77+
# Confirm the pod's VM was actually spawned from the factory/template
78+
# rather than booted normally. A normal VM stores its state directly under
79+
# /run/vc/vm/<sandbox-id>/ (a real directory), whereas a factory-spawned VM
80+
# is created under a generated UUID directory and /run/vc/vm/<sandbox-id> is
81+
# a symlink pointing at it (see assignSandbox() in
82+
# src/runtime/virtcontainers/vm.go). With templating enabled, the factory is
83+
# the template factory, so the symlink is our signal the template was used.
84+
local sandbox_id
85+
sandbox_id="$(exec_host "$node" \
86+
"crictl --runtime-endpoint unix:///run/containerd/containerd.sock pods --name ${pod_name} --state Ready -q | head -1")"
87+
88+
exec_host "$node" "test -L /run/vc/vm/${sandbox_id} && echo symlink" | grep -q symlink
89+
}
90+
91+
teardown() {
92+
vm_templating_supported || return 0
93+
94+
rm -f "${pod_config:-}"
95+
96+
# Destroy the VM template and remove the config drop-in on the target node.
97+
exec_host "$node" "sudo /opt/kata/bin/kata-runtime factory destroy" \
98+
|| echo "Warning: Failed to destroy VM template on node $node"
99+
100+
remove_kata_runtime_config_dropin_file "$node" "${dropin_path:-}" \
101+
|| echo "Warning: Failed to remove Kata runtime config drop-in on node $node"
102+
103+
teardown_common "${node:-}" "${node_start_time:-}"
104+
}

tests/integration/kubernetes/run_kubernetes_tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ else
104104
"k8s-security-context.bats" \
105105
"k8s-shared-volume.bats" \
106106
"k8s-volume.bats" \
107+
"k8s-vm-templating.bats" \
107108
"k8s-nginx-connectivity.bats" \
108109
)
109110

tools/packaging/kata-deploy/binary/src/artifacts/snapshotters.rs

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,50 @@ use std::path::Path;
1515
pub async fn configure_erofs_snapshotter(config: &Config, configuration_file: &Path) -> Result<()> {
1616
info!("Configuring erofs-snapshotter");
1717

18+
// "unmerged" mode keeps each image layer as its own per-layer `layer.erofs`
19+
// (containerd's default, non-fsmerged layout), which is the only layout the
20+
// Go runtime can consume. In the default "merged" mode we force containerd
21+
// to merge layers into a single `fsmeta.erofs`, which is runtime-rs only.
22+
let unmerged = config.erofs_merge_mode.as_deref() == Some("unmerged");
23+
1824
// The Go runtime does not support fsmerged EROFS (fsmeta.erofs).
1925
// If the snapshotter handler mapping explicitly pairs a Go shim with
20-
// erofs, that is a hard misconfiguration — bail out so the operator
21-
// fixes the mapping instead of hitting cryptic runtime errors later.
22-
if let Some(mapping) = config.snapshotter_handler_mapping_for_arch.as_ref() {
23-
let mut go_shims_on_erofs = Vec::new();
24-
for entry in mapping.split(',') {
25-
let parts: Vec<&str> = entry.split(':').collect();
26-
if parts.len() == 2 && parts[1] == "erofs" && !utils::is_rust_shim(parts[0]) {
27-
go_shims_on_erofs.push(parts[0].to_string());
26+
// erofs in the (default) merged mode, that is a hard misconfiguration —
27+
// bail out so the operator fixes the mapping instead of hitting cryptic
28+
// runtime errors later. In "unmerged" mode the Go runtime is supported, so
29+
// skip this guard.
30+
if !unmerged {
31+
if let Some(mapping) = config.snapshotter_handler_mapping_for_arch.as_ref() {
32+
let mut go_shims_on_erofs = Vec::new();
33+
for entry in mapping.split(',') {
34+
let parts: Vec<&str> = entry.split(':').collect();
35+
if parts.len() == 2 && parts[1] == "erofs" && !utils::is_rust_shim(parts[0]) {
36+
go_shims_on_erofs.push(parts[0].to_string());
37+
}
2838
}
29-
}
30-
if !go_shims_on_erofs.is_empty() {
31-
warn!("##########################################################################");
32-
warn!("# #");
33-
warn!("# Go runtime shim(s) mapped to the erofs snapshotter: #");
34-
for s in &go_shims_on_erofs {
35-
warn!("# - {:<64} #", s);
39+
if !go_shims_on_erofs.is_empty() {
40+
warn!("##########################################################################");
41+
warn!("# #");
42+
warn!("# Go runtime shim(s) mapped to the erofs snapshotter: #");
43+
for s in &go_shims_on_erofs {
44+
warn!("# - {:<64} #", s);
45+
}
46+
warn!("# #");
47+
warn!(
48+
"# The Go runtime does NOT support fsmerged EROFS (fsmeta.erofs). #"
49+
);
50+
warn!("# Only runtime-rs shims are supported with merged erofs. Set #");
51+
warn!("# EROFS_MERGE_MODE=unmerged to use the Go runtime with erofs. #");
52+
warn!("# #");
53+
warn!("##########################################################################");
54+
return Err(anyhow::anyhow!(
55+
"erofs snapshotter: Go runtime shim(s) [{}] cannot be mapped to merged erofs. \
56+
The Go runtime does not support fsmerged EROFS. \
57+
Set EROFS_MERGE_MODE=unmerged, remove these shims from \
58+
SNAPSHOTTER_HANDLER_MAPPING, or switch them to runtime-rs.",
59+
go_shims_on_erofs.join(", ")
60+
));
3661
}
37-
warn!("# #");
38-
warn!("# The Go runtime does NOT support fsmerged EROFS (fsmeta.erofs). #");
39-
warn!("# Only runtime-rs shims are supported with the erofs snapshotter. #");
40-
warn!("# #");
41-
warn!("##########################################################################");
42-
return Err(anyhow::anyhow!(
43-
"erofs snapshotter: Go runtime shim(s) [{}] cannot be mapped to erofs. \
44-
The Go runtime does not support fsmerged EROFS. \
45-
Remove these shims from SNAPSHOTTER_HANDLER_MAPPING or switch them to runtime-rs.",
46-
go_shims_on_erofs.join(", ")
47-
));
4862
}
4963
}
5064

@@ -88,11 +102,27 @@ pub async fn configure_erofs_snapshotter(config: &Config, configuration_file: &P
88102
".plugins.\"io.containerd.snapshotter.v1.erofs\".default_size",
89103
"\"10G\"",
90104
)?;
91-
toml_utils::set_toml_value(
92-
configuration_file,
93-
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
94-
"0",
95-
)?;
105+
// In the default "merged" mode, force containerd to merge all layers into a
106+
// single fsmeta.erofs (max_unmerged_layers = 0). In "unmerged" mode we delete
107+
// any previously-written value so each layer stays a separate layer.erofs,
108+
// which the Go runtime requires.
109+
//
110+
// Because kata-deploy edits the containerd config in place, switching from
111+
// merged to unmerged must actively remove the old `max_unmerged_layers = 0`
112+
// left behind by a previous install. Otherwise the stale `0` would keep
113+
// forcing the merged layout and break Go-runtime compatibility.
114+
if !unmerged {
115+
toml_utils::set_toml_value(
116+
configuration_file,
117+
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
118+
"0",
119+
)?;
120+
} else {
121+
toml_utils::delete_toml_value(
122+
configuration_file,
123+
".plugins.\"io.containerd.snapshotter.v1.erofs\".max_unmerged_layers",
124+
)?;
125+
}
96126

97127
Ok(())
98128
}

0 commit comments

Comments
 (0)