Merge pull request #13664 from samuelkarp/criu-check-fail-fast

Disable checkpoint restore codepath when CRIU is not installed
This commit is contained in:
Samuel Karp
2026-07-08 06:10:01 +00:00
committed by GitHub
14 changed files with 651 additions and 12 deletions

View File

@@ -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

View File

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

View 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

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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)
}

View File

@@ -109,5 +109,6 @@ func DefaultRuntimeConfig() RuntimeConfig {
DrainExecSyncIOTimeout: "0s",
EnableUnprivilegedPorts: true,
EnableUnprivilegedICMP: true,
EnableCRIU: func() *bool { v := true; return &v }(),
}
}

View File

@@ -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 }(),
}
}

View 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`)
}

View File

@@ -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{

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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()