mirror of
https://github.com/containerd/containerd.git
synced 2026-08-07 08:31:18 +00:00
Merge pull request #13664 from samuelkarp/criu-check-fail-fast
Disable checkpoint restore codepath when CRIU is not installed
This commit is contained in:
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -564,6 +564,15 @@ jobs:
|
||||
env
|
||||
sudo -E PATH=$PATH ./contrib/checkpoint/checkpoint-restore-cri-test.sh
|
||||
|
||||
- if: matrix.os != 'ubuntu-24.04-arm'
|
||||
name: Checkpoint/Restore Disable via CRI
|
||||
env:
|
||||
TEST_RUNTIME: ${{ matrix.runtime }}
|
||||
CGROUP_DRIVER: ${{ matrix.cgroup_driver }}
|
||||
run: |
|
||||
env
|
||||
sudo -E PATH=$PATH ./contrib/checkpoint/checkpoint-restore-cri-disable-test.sh
|
||||
|
||||
# Log the status of this VM to investigate issues like
|
||||
# https://github.com/containerd/containerd/issues/4969
|
||||
- name: Host Status
|
||||
|
||||
6
.github/workflows/node-e2e.yml
vendored
6
.github/workflows/node-e2e.yml
vendored
@@ -64,6 +64,12 @@ jobs:
|
||||
echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV
|
||||
echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install criu
|
||||
run: |
|
||||
sudo add-apt-repository -y ppa:criu/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y criu
|
||||
|
||||
- name: Build and Install containerd
|
||||
working-directory: ./src/github.com/containerd/containerd
|
||||
run: |
|
||||
|
||||
303
contrib/checkpoint/checkpoint-restore-cri-disable-test.sh
Executable file
303
contrib/checkpoint/checkpoint-restore-cri-disable-test.sh
Executable file
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright The containerd Authors.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
DIR="$(dirname "${0}")"
|
||||
cd "${DIR}"
|
||||
if ! command -v crictl >/dev/null 2>&1; then
|
||||
echo >&2 "ERROR: crictl binary not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
crictl() {
|
||||
command crictl --timeout 60s "$@"
|
||||
}
|
||||
TESTDIR=$(mktemp -d -p "${PWD}")
|
||||
SUCCESS=0
|
||||
CRIU_PATH=$(command -v criu || true)
|
||||
CRIU_DIR=""
|
||||
if [ -n "${CRIU_PATH}" ]; then
|
||||
CRIU_DIR=$(dirname "${CRIU_PATH}")
|
||||
fi
|
||||
|
||||
function get_cleaned_path() {
|
||||
if [ -z "${CRIU_DIR}" ]; then
|
||||
echo "$PATH"
|
||||
return
|
||||
fi
|
||||
local real_criu_dir
|
||||
real_criu_dir=$(realpath "${CRIU_DIR}")
|
||||
local shadow_dir="${TESTDIR}/shadow_path"
|
||||
mkdir -p "${shadow_dir}"
|
||||
for file in "${real_criu_dir}"/*; do
|
||||
if [ -x "$file" ] && [ "$(basename "$file")" != "criu" ]; then
|
||||
ln -s "$file" "${shadow_dir}/" || true
|
||||
fi
|
||||
done
|
||||
local cleaned=""
|
||||
local IFS=':'
|
||||
for dir in $PATH; do
|
||||
if [ -d "$dir" ]; then
|
||||
local real_dir
|
||||
real_dir=$(realpath "$dir")
|
||||
if [ "${real_dir}" == "${real_criu_dir}" ]; then
|
||||
cleaned="${cleaned:+$cleaned:}${shadow_dir}"
|
||||
else
|
||||
cleaned="${cleaned:+$cleaned:}$dir"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "${cleaned}"
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
crictl ps -a || true
|
||||
stop_containerd
|
||||
if [ "${SUCCESS}" == "1" ]; then
|
||||
echo PASS
|
||||
else
|
||||
echo "--> containerd logs"
|
||||
if [ -f "${TESTDIR}/containerd.log" ]; then
|
||||
sed 's/^/----> \t/' <"${TESTDIR}/containerd.log"
|
||||
fi
|
||||
echo FAIL
|
||||
fi
|
||||
find "${TESTDIR}" -name shm -type d -exec umount {} + >/dev/null 2>&1 || true
|
||||
find "${TESTDIR}" -name rootfs -type d -exec umount {} + >/dev/null 2>&1 || true
|
||||
rm -rf "${TESTDIR}" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
export CONTAINERD_ADDRESS="$TESTDIR/c.sock"
|
||||
export CONTAINER_RUNTIME_ENDPOINT="unix://${CONTAINERD_ADDRESS}"
|
||||
TEST_IMAGE=ghcr.io/containerd/alpine
|
||||
TESTDATA=testdata
|
||||
function start_containerd() {
|
||||
local enable_criu="$1"
|
||||
local use_path="$2"
|
||||
echo "--> Starting containerd with enable_criu=${enable_criu}"
|
||||
cat >"${TESTDIR}/config.toml" <<EOF
|
||||
version = 3
|
||||
[plugins."io.containerd.cri.v1.runtime"]
|
||||
EOF
|
||||
if [ "${enable_criu}" != "omit" ]; then
|
||||
cat >>"${TESTDIR}/config.toml" <<EOF
|
||||
enable_criu = ${enable_criu}
|
||||
EOF
|
||||
fi
|
||||
cat >>"${TESTDIR}/config.toml" <<EOF
|
||||
[plugins."io.containerd.cri.v1.runtime".containerd]
|
||||
default_runtime_name = "test-runtime"
|
||||
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.test-runtime]
|
||||
runtime_type = "${TEST_RUNTIME:-io.containerd.runc.v2}"
|
||||
EOF
|
||||
mkdir -p "${TESTDIR}"/{root,state}
|
||||
echo "=== STARTING CONTAINERD (enable_criu=${enable_criu}) ===" >> "${TESTDIR}/containerd.log"
|
||||
if [ -n "${use_path}" ]; then
|
||||
env PATH="${use_path}" ../../bin/containerd \
|
||||
--address "${TESTDIR}/c.sock" \
|
||||
--config "${TESTDIR}/config.toml" \
|
||||
--root "${TESTDIR}/root" \
|
||||
--state "${TESTDIR}/state" \
|
||||
--log-level trace >>"${TESTDIR}/containerd.log" 2>&1 &
|
||||
else
|
||||
../../bin/containerd \
|
||||
--address "${TESTDIR}/c.sock" \
|
||||
--config "${TESTDIR}/config.toml" \
|
||||
--root "${TESTDIR}/root" \
|
||||
--state "${TESTDIR}/state" \
|
||||
--log-level trace >>"${TESTDIR}/containerd.log" 2>&1 &
|
||||
fi
|
||||
echo $! > "${TESTDIR}/containerd.pid"
|
||||
retry_counter=0
|
||||
max_retries=10
|
||||
while true; do
|
||||
((retry_counter += 1))
|
||||
if crictl info >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
if [ "${retry_counter}" -gt "${max_retries}" ]; then
|
||||
echo "--> Failed to start containerd"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
function stop_containerd() {
|
||||
echo "--> Stopping containerd..."
|
||||
if [ -f "${TESTDIR}/containerd.pid" ]; then
|
||||
local pid
|
||||
pid=$(cat "${TESTDIR}/containerd.pid")
|
||||
echo "--> Killing containerd PID ${pid}..."
|
||||
kill -15 "${pid}" || true
|
||||
sleep 2
|
||||
if kill -0 "${pid}" 2>/dev/null; then
|
||||
echo "--> containerd PID ${pid} still running, force killing..."
|
||||
kill -9 "${pid}" || true
|
||||
fi
|
||||
rm -f "${TESTDIR}/containerd.pid"
|
||||
else
|
||||
echo "--> No containerd.pid found, using pkill..."
|
||||
pkill -x containerd || true
|
||||
sleep 2
|
||||
fi
|
||||
}
|
||||
function setup_container() {
|
||||
crictl pull "${TEST_IMAGE}" >/dev/null
|
||||
POD_JSON=$(mktemp)
|
||||
jq ".log_directory=\"${TESTDIR}\"" "$TESTDATA"/sandbox_config.json >"$POD_JSON"
|
||||
pod_id=$(crictl runp "$POD_JSON")
|
||||
ctr_id=$(crictl create "$pod_id" "$TESTDATA"/container_sleep.json "$POD_JSON")
|
||||
crictl start "$ctr_id" >/dev/null
|
||||
rm -f "$POD_JSON"
|
||||
echo "${pod_id}:${ctr_id}"
|
||||
}
|
||||
function cleanup_container() {
|
||||
local pod_id="$1"
|
||||
local ctr_id="$2"
|
||||
crictl rm -f "${ctr_id}" >/dev/null 2>&1 || true
|
||||
crictl rmp -f "${pod_id}" >/dev/null 2>&1 || true
|
||||
crictl rmi "${TEST_IMAGE}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# --- Test Executions ---
|
||||
rm -f "${TESTDIR}/containerd.log"
|
||||
|
||||
# ==============================================================================
|
||||
# Group 1 (Configuration: enable_criu = false, normal PATH)
|
||||
# ==============================================================================
|
||||
start_containerd "false" ""
|
||||
|
||||
# Test 1: Checkpoint fails fast when enable_criu = false, and the source container remains running.
|
||||
ids=$(setup_container)
|
||||
pod_id=$(echo "$ids" | cut -d: -f1)
|
||||
ctr_id=$(echo "$ids" | cut -d: -f2)
|
||||
set +e
|
||||
output=$(crictl checkpoint --export="$TESTDIR"/cp.tar "${ctr_id}" 2>&1)
|
||||
exit_code=$?
|
||||
set -e
|
||||
if [ $exit_code -eq 0 ] || [[ ! "$output" =~ "criu support is disabled by configuration" ]]; then
|
||||
echo "ERROR: Test 1 failed (checkpoint did not fail fast). Output: $output"
|
||||
exit 1
|
||||
fi
|
||||
state=$(crictl inspect "${ctr_id}" | jq -r '.status.state')
|
||||
if [ "$state" != "CONTAINER_RUNNING" ]; then
|
||||
echo "ERROR: Test 1 failed (source container state is not RUNNING). State: $state"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Test 1: Checkpoint fails fast and source container remains running"
|
||||
cleanup_container "$pod_id" "$ctr_id"
|
||||
|
||||
# Test 2: Restore fails fast when enable_criu = false.
|
||||
POD_JSON=$(mktemp)
|
||||
jq ".log_directory=\"${TESTDIR}\"" "$TESTDATA"/sandbox_config.json >"$POD_JSON"
|
||||
pod_id=$(crictl runp "$POD_JSON")
|
||||
touch "$TESTDIR/dummy-checkpoint.tar"
|
||||
RESTORE_JSON=$(mktemp)
|
||||
jq ".image.image=\"$TESTDIR/dummy-checkpoint.tar\"" "$TESTDATA"/container_sleep.json >"$RESTORE_JSON"
|
||||
set +e
|
||||
output=$(crictl create "$pod_id" "$RESTORE_JSON" "$POD_JSON" 2>&1)
|
||||
exit_code=$?
|
||||
set -e
|
||||
rm -f "$RESTORE_JSON" "$POD_JSON"
|
||||
if [ $exit_code -eq 0 ] || [[ ! "$output" =~ "criu support is disabled by configuration" ]]; then
|
||||
echo "ERROR: Test 2 failed (restore did not fail fast). Output: $output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Test 2: Restore fails fast when enable_criu = false"
|
||||
crictl rmp -f "$pod_id" >/dev/null 2>&1 || true
|
||||
|
||||
stop_containerd
|
||||
|
||||
# ==============================================================================
|
||||
# Group 2 (Configuration: enable_criu = true, normal PATH)
|
||||
# ==============================================================================
|
||||
start_containerd "true" ""
|
||||
|
||||
# Test 3: Normal checkpoint and restore preserves container state.
|
||||
ids=$(setup_container)
|
||||
pod_id=$(echo "$ids" | cut -d: -f1)
|
||||
ctr_id=$(echo "$ids" | cut -d: -f2)
|
||||
crictl exec "$ctr_id" touch /root/state_file
|
||||
rm -f "$TESTDIR"/state_checkpoint.tar
|
||||
crictl checkpoint --export="$TESTDIR"/state_checkpoint.tar "${ctr_id}"
|
||||
cleanup_container "$pod_id" "$ctr_id"
|
||||
|
||||
POD_JSON=$(mktemp)
|
||||
jq ".log_directory=\"${TESTDIR}\"" "$TESTDATA"/sandbox_config.json >"$POD_JSON"
|
||||
pod_id=$(crictl runp "$POD_JSON")
|
||||
RESTORE_JSON=$(mktemp)
|
||||
jq ".image.image=\"$TESTDIR/state_checkpoint.tar\"" "$TESTDATA"/container_sleep.json >"$RESTORE_JSON"
|
||||
restored_ctr_id=$(crictl create "$pod_id" "$RESTORE_JSON" "$POD_JSON")
|
||||
rm -f "$RESTORE_JSON" "$POD_JSON"
|
||||
crictl start "$restored_ctr_id"
|
||||
set +e
|
||||
crictl exec "$restored_ctr_id" ls /root/state_file >/dev/null 2>&1
|
||||
exit_code=$?
|
||||
set -e
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo "ERROR: Test 3 failed (state_file not found in restored container)."
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Test 3: Normal checkpoint and restore preserves container state"
|
||||
cleanup_container "$pod_id" "$restored_ctr_id"
|
||||
|
||||
stop_containerd
|
||||
|
||||
# ==============================================================================
|
||||
# Group 3 (Configuration: enable_criu omitted/defaults, normal PATH)
|
||||
# ==============================================================================
|
||||
start_containerd "omit" ""
|
||||
|
||||
# Test 4: enable_criu omitted from configuration defaults to true (allowing checkpoint/restore).
|
||||
ids=$(setup_container)
|
||||
pod_id=$(echo "$ids" | cut -d: -f1)
|
||||
ctr_id=$(echo "$ids" | cut -d: -f2)
|
||||
rm -f "$TESTDIR"/omitted_checkpoint.tar
|
||||
crictl checkpoint --export="$TESTDIR"/omitted_checkpoint.tar "${ctr_id}"
|
||||
echo "PASS: Test 4: enable_criu omitted from configuration defaults to true"
|
||||
cleanup_container "$pod_id" "$ctr_id"
|
||||
|
||||
stop_containerd
|
||||
|
||||
# ==============================================================================
|
||||
# Group 4 (Configuration: enable_criu = true, cleaned PATH without CRIU)
|
||||
# ==============================================================================
|
||||
if [ -n "${CRIU_DIR}" ]; then
|
||||
CLEANED_PATH=$(get_cleaned_path)
|
||||
|
||||
start_containerd "true" "${CLEANED_PATH}"
|
||||
|
||||
# Test 5: CRIU missing from PATH, enable_criu = true.
|
||||
# Verifies that when CRIU is enabled but missing, it fails with the binary missing error.
|
||||
ids=$(setup_container)
|
||||
pod_id=$(echo "$ids" | cut -d: -f1)
|
||||
ctr_id=$(echo "$ids" | cut -d: -f2)
|
||||
set +e
|
||||
output=$(crictl checkpoint --export="$TESTDIR"/cp.tar "${ctr_id}" 2>&1)
|
||||
exit_code=$?
|
||||
set -e
|
||||
if [ $exit_code -eq 0 ] || [[ ! "$output" =~ "criu binary not found" ]]; then
|
||||
echo "ERROR: Test 5 failed. Output: $output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Test 5: Fails with binary not found as expected when enable_criu = true"
|
||||
cleanup_container "$pod_id" "$ctr_id"
|
||||
|
||||
stop_containerd
|
||||
fi
|
||||
|
||||
SUCCESS=1
|
||||
@@ -194,6 +194,16 @@ func (m *ShimManager) ID() string {
|
||||
return plugins.ShimPlugin.String() + ".manager"
|
||||
}
|
||||
|
||||
// Env returns the environment configured for the shim manager.
|
||||
func (m *ShimManager) Env() []string {
|
||||
if m.env == nil {
|
||||
return nil
|
||||
}
|
||||
cp := make([]string, len(m.env))
|
||||
copy(cp, m.env)
|
||||
return cp
|
||||
}
|
||||
|
||||
// Start launches a new shim instance
|
||||
func (m *ShimManager) Start(ctx context.Context, id string, bundle *Bundle, opts runtime.CreateOpts) (_ ShimInstance, retErr error) {
|
||||
shouldInvokeShimBinary := false
|
||||
|
||||
@@ -274,6 +274,7 @@ version = 3
|
||||
ignore_deprecation_warnings = []
|
||||
stats_collect_period = '1s'
|
||||
stats_retention_period = '2m'
|
||||
enable_criu = true
|
||||
|
||||
[plugins.'io.containerd.cri.v1.runtime'.containerd]
|
||||
default_runtime_name = 'runc'
|
||||
|
||||
@@ -447,6 +447,10 @@ type RuntimeConfig struct {
|
||||
// https://golang.org/pkg/time/#ParseDuration
|
||||
// Default: "2m"
|
||||
StatsRetentionPeriod string `toml:"stats_retention_period" json:"statsRetentionPeriod"`
|
||||
|
||||
// EnableCRIU enables CRIU (Checkpoint/Restore In Userspace) support.
|
||||
// When set to false, checkpoint/restore operations will be disabled.
|
||||
EnableCRIU *bool `toml:"enable_criu" json:"enableCRIU"`
|
||||
}
|
||||
|
||||
// X509KeyPairStreaming contains the x509 configuration for streaming
|
||||
|
||||
@@ -531,3 +531,12 @@ func TestCheckLocalImagePullConfigs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfigEnableCRIU(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("only supported on Linux")
|
||||
}
|
||||
cfg := DefaultRuntimeConfig()
|
||||
assert.NotNil(t, cfg.EnableCRIU)
|
||||
assert.True(t, *cfg.EnableCRIU)
|
||||
}
|
||||
|
||||
@@ -109,5 +109,6 @@ func DefaultRuntimeConfig() RuntimeConfig {
|
||||
DrainExecSyncIOTimeout: "0s",
|
||||
EnableUnprivilegedPorts: true,
|
||||
EnableUnprivilegedICMP: true,
|
||||
EnableCRIU: func() *bool { v := true; return &v }(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,5 +87,6 @@ func DefaultRuntimeConfig() RuntimeConfig {
|
||||
// TODO(windows): Add platform specific config, so that most common defaults can be shared.
|
||||
|
||||
DrainExecSyncIOTimeout: "0s",
|
||||
EnableCRIU: func() *bool { v := false; return &v }(),
|
||||
}
|
||||
}
|
||||
|
||||
84
internal/cri/config/enable_criu_linux_test.go
Normal file
84
internal/cri/config/enable_criu_linux_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseEnableCRIU(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tomlStr string
|
||||
expectedCRIU *bool
|
||||
}{
|
||||
{
|
||||
name: "enable_criu set to true",
|
||||
tomlStr: `
|
||||
enable_criu = true
|
||||
`,
|
||||
expectedCRIU: func() *bool { v := true; return &v }(),
|
||||
},
|
||||
{
|
||||
name: "enable_criu set to false",
|
||||
tomlStr: `
|
||||
enable_criu = false
|
||||
`,
|
||||
expectedCRIU: func() *bool { v := false; return &v }(),
|
||||
},
|
||||
{
|
||||
name: "enable_criu absent",
|
||||
tomlStr: `
|
||||
# empty or other fields
|
||||
`,
|
||||
expectedCRIU: func() *bool { v := true; return &v }(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := DefaultRuntimeConfig()
|
||||
err := toml.Unmarshal([]byte(tc.tomlStr), &cfg)
|
||||
assert.NoError(t, err)
|
||||
if tc.expectedCRIU == nil {
|
||||
assert.Nil(t, cfg.EnableCRIU)
|
||||
} else {
|
||||
if assert.NotNil(t, cfg.EnableCRIU) {
|
||||
assert.Equal(t, *tc.expectedCRIU, *cfg.EnableCRIU)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONEnableCRIU(t *testing.T) {
|
||||
jsonStr := `{"enableCRIU": false}`
|
||||
cfg := DefaultRuntimeConfig()
|
||||
err := json.Unmarshal([]byte(jsonStr), &cfg)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, cfg.EnableCRIU) {
|
||||
assert.False(t, *cfg.EnableCRIU)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(b), `"enableCRIU":false`)
|
||||
}
|
||||
@@ -26,11 +26,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
crmetadata "github.com/checkpoint-restore/checkpointctl/lib"
|
||||
criu "github.com/checkpoint-restore/go-criu/v7"
|
||||
"github.com/checkpoint-restore/go-criu/v7/utils"
|
||||
"github.com/containerd/containerd/api/types/runc/options"
|
||||
"github.com/containerd/containerd/v2/client"
|
||||
@@ -135,6 +137,58 @@ func assertCheckpointDirSafe(root string) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (c *criService) checkCriu() error {
|
||||
c.checkCriuOnce.Do(func() {
|
||||
c.checkCriuErr = c.doCheckCriu()
|
||||
})
|
||||
return c.checkCriuErr
|
||||
}
|
||||
|
||||
func (c *criService) doCheckCriu() error {
|
||||
if c.config.EnableCRIU != nil && !*c.config.EnableCRIU {
|
||||
return errors.New("criu support is disabled by configuration")
|
||||
}
|
||||
path := resolveCriuPath(c.shimPath)
|
||||
if path == "" {
|
||||
return errors.New("criu binary not found in shim path or system PATH")
|
||||
}
|
||||
client := criu.MakeCriu()
|
||||
client.SetCriuPath(path)
|
||||
version, err := client.GetCriuVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve criu version: %w", err)
|
||||
}
|
||||
if version < utils.PodCriuVersion {
|
||||
return fmt.Errorf("checkpoint/restore requires at least CRIU %d, current version is %d", utils.PodCriuVersion, version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveCriuPath(customPath string) string {
|
||||
if customPath != "" {
|
||||
// This logic is Linux-specific. If CRIU is ever supported on other
|
||||
// operating systems, path lookup will need to respect that OS's
|
||||
// conventions.
|
||||
for _, dir := range filepath.SplitList(customPath) {
|
||||
if !filepath.IsAbs(dir) {
|
||||
continue
|
||||
}
|
||||
criuPath := filepath.Join(dir, "criu")
|
||||
if fi, err := os.Stat(criuPath); err == nil && fi.Mode().IsRegular() && fi.Mode()&0111 != 0 {
|
||||
return criuPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if criuPath, err := exec.LookPath("criu"); err == nil {
|
||||
if absPath, err := filepath.Abs(criuPath); err == nil {
|
||||
return absPath
|
||||
}
|
||||
return criuPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// checkIfCheckpointOCIImage returns checks if the input refers to a checkpoint image.
|
||||
// It returns the StorageImageID of the image the input resolves to, nil otherwise.
|
||||
func (c *criService) checkIfCheckpointOCIImage(ctx context.Context, input string) (string, error) {
|
||||
@@ -189,6 +243,10 @@ func (c *criService) CRImportCheckpoint(
|
||||
sandbox *sandbox.Sandbox,
|
||||
sandboxConfig *runtime.PodSandboxConfig,
|
||||
) (ctrID string, retErr error) {
|
||||
if err := c.checkCriu(); err != nil {
|
||||
return "", fmt.Errorf("checkpoint restore is not enabled: %w", err)
|
||||
}
|
||||
|
||||
var mountPoint string
|
||||
start := time.Now()
|
||||
// Ensure that the image to restore the checkpoint from has been provided.
|
||||
@@ -539,18 +597,9 @@ func (c *criService) CRImportCheckpoint(
|
||||
|
||||
func (c *criService) CheckpointContainer(ctx context.Context, r *runtime.CheckpointContainerRequest) (*runtime.CheckpointContainerResponse, error) {
|
||||
start := time.Now()
|
||||
if err := utils.CheckForCriu(utils.PodCriuVersion); err != nil {
|
||||
errorMessage := fmt.Sprintf(
|
||||
"CRIU binary not found or too old (<%d). Failed to checkpoint container %q",
|
||||
utils.PodCriuVersion,
|
||||
r.GetContainerId(),
|
||||
)
|
||||
log.G(ctx).WithError(err).Error(errorMessage)
|
||||
return nil, fmt.Errorf(
|
||||
"%s: %w",
|
||||
errorMessage,
|
||||
err,
|
||||
)
|
||||
if err := c.checkCriu(); err != nil {
|
||||
log.G(ctx).WithError(err).Errorf("Failed to checkpoint container %q", r.GetContainerId())
|
||||
return nil, fmt.Errorf("failed to checkpoint container %q: %w", r.GetContainerId(), err)
|
||||
}
|
||||
|
||||
criContainerStatus, err := c.ContainerStatus(ctx, &runtime.ContainerStatusRequest{
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
)
|
||||
|
||||
func TestCopyNoFollowRegularFile(t *testing.T) {
|
||||
@@ -235,3 +237,135 @@ func TestFilterAndMergeAnnotations(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCriuPath(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "criu-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a dummy executable criu
|
||||
execDir := filepath.Join(tempDir, "bin-exec")
|
||||
if err := os.MkdirAll(execDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
execPath := filepath.Join(execDir, "criu")
|
||||
if err := os.WriteFile(execPath, []byte("dummy"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a dummy non-executable criu
|
||||
nonExecDir := filepath.Join(tempDir, "bin-nonexec")
|
||||
if err := os.MkdirAll(nonExecDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nonExecPath := filepath.Join(nonExecDir, "criu")
|
||||
if err := os.WriteFile(nonExecPath, []byte("dummy"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
relDir, err := filepath.Rel(wd, execDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mock the system PATH to point to our executable directory
|
||||
t.Setenv("PATH", execDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
customPath string
|
||||
expectedPath string
|
||||
}{
|
||||
{
|
||||
name: "custom PATH with executable criu",
|
||||
customPath: execDir,
|
||||
expectedPath: execPath,
|
||||
},
|
||||
{
|
||||
name: "custom PATH with non-executable criu",
|
||||
customPath: nonExecDir,
|
||||
expectedPath: "",
|
||||
},
|
||||
{
|
||||
name: "multiple directories in PATH, executable in second",
|
||||
customPath: nonExecDir + string(filepath.ListSeparator) + execDir,
|
||||
expectedPath: execPath,
|
||||
},
|
||||
{
|
||||
name: "custom PATH with relative directory is skipped",
|
||||
customPath: relDir,
|
||||
expectedPath: "",
|
||||
},
|
||||
{
|
||||
name: "empty customPath falls back to system PATH",
|
||||
customPath: "",
|
||||
expectedPath: execPath, // Falls back to system PATH (mocked to execDir)
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := resolveCriuPath(tt.customPath)
|
||||
if path != tt.expectedPath {
|
||||
t.Errorf("expected %s, got %s", tt.expectedPath, path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckCriuDisabled(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
c.config.EnableCRIU = func() *bool { v := false; return &v }()
|
||||
err := c.checkCriu()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "criu support is disabled by configuration") {
|
||||
t.Errorf("expected error containing 'criu support is disabled by configuration', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckCriuEnabled(t *testing.T) {
|
||||
t.Setenv("PATH", "")
|
||||
c := newTestCRIService()
|
||||
c.config.EnableCRIU = func() *bool { v := true; return &v }()
|
||||
err := c.checkCriu()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if strings.Contains(err.Error(), "criu support is disabled by configuration") {
|
||||
t.Errorf("did not expect error containing 'criu support is disabled by configuration', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointContainerDisabled(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
c.config.EnableCRIU = func() *bool { v := false; return &v }()
|
||||
_, err := c.CheckpointContainer(context.Background(), &runtime.CheckpointContainerRequest{
|
||||
ContainerId: "test-container",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "criu support is disabled by configuration") {
|
||||
t.Errorf("expected error containing 'criu support is disabled by configuration', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRImportCheckpointDisabled(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
c.config.EnableCRIU = func() *bool { v := false; return &v }()
|
||||
_, err := c.CRImportCheckpoint(context.Background(), nil, nil, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "criu support is disabled by configuration") {
|
||||
t.Errorf("expected error containing 'criu support is disabled by configuration', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,11 @@ type criService struct {
|
||||
runtimeFeatures *runtime.RuntimeFeatures
|
||||
// statsCollector collects CPU stats in background for UsageNanoCores calculation
|
||||
statsCollector *StatsCollector
|
||||
// shimPath is the custom PATH environment variable value from the shim manager
|
||||
shimPath string
|
||||
|
||||
checkCriuOnce sync.Once //nolint:nolintlint,unused // Ignore on non-Linux
|
||||
checkCriuErr error //nolint:nolintlint,unused // Ignore on non-Linux
|
||||
}
|
||||
|
||||
type CRIServiceOptions struct {
|
||||
@@ -187,6 +192,9 @@ type CRIServiceOptions struct {
|
||||
//
|
||||
// TODO: Replace this gradually with directly configured instances
|
||||
Client *containerd.Client
|
||||
|
||||
// ShimPath is the custom PATH environment variable value from the shim manager
|
||||
ShimPath string
|
||||
}
|
||||
|
||||
// NewCRIService returns a new instance of CRIService
|
||||
@@ -214,6 +222,7 @@ func NewCRIService(options *CRIServiceOptions) (CRIService, runtime.RuntimeServi
|
||||
sandboxService: newCriSandboxService(&config, options.SandboxControllers),
|
||||
runtimeHandlers: make(map[string]*runtime.RuntimeHandler),
|
||||
statsCollector: statsCollector,
|
||||
shimPath: options.ShimPath,
|
||||
}
|
||||
|
||||
// TODO: Make discard time configurable
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/containerd/plugin"
|
||||
@@ -58,6 +59,7 @@ func init() {
|
||||
plugins.SandboxStorePlugin,
|
||||
plugins.TransferPlugin,
|
||||
plugins.WarningPlugin,
|
||||
plugins.ShimPlugin,
|
||||
},
|
||||
Config: &defaultConfig,
|
||||
ConfigMigration: configMigration,
|
||||
@@ -131,6 +133,22 @@ func initCRIService(ic *plugin.InitContext) (any, error) {
|
||||
return nil, fmt.Errorf("failed to get streaming config: %w", err)
|
||||
}
|
||||
|
||||
var shimPath string
|
||||
shimPlugin, err := ic.GetSingle(plugins.ShimPlugin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get shim plugin: %w", err)
|
||||
}
|
||||
if hasEnv, ok := shimPlugin.(interface{ Env() []string }); ok {
|
||||
env := hasEnv.Env()
|
||||
for i := len(env) - 1; i >= 0; i-- {
|
||||
// iterate backwards to grab the last PATH=
|
||||
if path, ok := strings.CutPrefix(env[i], "PATH="); ok {
|
||||
shimPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options := &server.CRIServiceOptions{
|
||||
RuntimeService: runtimeSvc,
|
||||
ImageService: imageSvc,
|
||||
@@ -138,6 +156,7 @@ func initCRIService(ic *plugin.InitContext) (any, error) {
|
||||
NRI: getNRIAPI(ic),
|
||||
Client: client,
|
||||
SandboxControllers: sbControllers,
|
||||
ShimPath: shimPath,
|
||||
}
|
||||
is := criImagePlugin.(imageService).GRPCService()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user