mirror of
https://github.com/containerd/containerd.git
synced 2026-08-09 09:33:06 +00:00
cri: validate CRIU availability and version early
Perform an early validation check on both container checkpoint and restore paths to fail-fast if the CRIU binary is missing or is older than the minimum required version (3.16.0). To support runtime-configured environments, the validation respects the custom PATH from the shim manager environment if configured, skipping any relative paths to avoid incorrect daemon-relative resolution. If not configured, it falls back to a standard system PATH lookup. The check result is cached using sync.Once to prevent redundant process spawning. Assisted-by: Antigravity Signed-off-by: Samuel Karp <samuelkarp@google.com>
This commit is contained in:
committed by
k8s-infra-cherrypick-robot
parent
5e5581f651
commit
dacd4c7d00
@@ -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
|
||||
|
||||
@@ -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,55 @@ 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 {
|
||||
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 +240,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 +594,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{
|
||||
|
||||
@@ -235,3 +235,84 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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