mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 13:27:16 +00:00
Remove support for restoring checkpoint data during CreateContainer, which was previously deprecated in v2.3. Assisted-by: Antigravity Signed-off-by: Samuel Karp <samuelkarp@google.com>
429 lines
13 KiB
Go
429 lines
13 KiB
Go
//go:build linux
|
|
|
|
/*
|
|
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 server
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"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"
|
|
"github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/pkg/archive"
|
|
"github.com/containerd/containerd/v2/pkg/protobuf/proto"
|
|
ptypes "github.com/containerd/containerd/v2/pkg/protobuf/types"
|
|
"github.com/containerd/containerd/v2/plugins"
|
|
"github.com/containerd/log"
|
|
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"golang.org/x/sys/unix"
|
|
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
|
|
|
// TODO: This package import is kept to prevent merge conflicts while integrating multiple
|
|
// branches, specifically because this changes vendoring.
|
|
_ "github.com/checkpoint-restore/go-criu/v7/stats"
|
|
)
|
|
|
|
// copyNoFollow copies the regular file at src to dst without following a symlink
|
|
// at the final path component of src.
|
|
//
|
|
// The checkpoint code reads files (container.log, status, stats-dump, dump.log)
|
|
// out of the container state directory, which can contain entries unpacked from a
|
|
// checkpoint archive or OCI image. Those entries are externally provided, so they
|
|
// are read defensively.
|
|
//
|
|
// src is first lstat'd (which does not follow a final-component symlink) and must
|
|
// be a regular file; non-regular entries are rejected before src is ever opened.
|
|
// src is then opened with O_NOFOLLOW as a belt-and-suspenders guard in case the
|
|
// entry changes type between the lstat and the open.
|
|
func copyNoFollow(src, dst string, perm os.FileMode) error {
|
|
fi, err := os.Lstat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !fi.Mode().IsRegular() {
|
|
return fmt.Errorf("refusing to copy %s: not a regular file", src)
|
|
}
|
|
|
|
in, err := os.OpenFile(src, os.O_RDONLY|unix.O_NOFOLLOW, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
|
|
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
_, err = io.Copy(out, in)
|
|
return err
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
|
|
func (c *criService) CheckpointContainer(ctx context.Context, r *runtime.CheckpointContainerRequest) (*runtime.CheckpointContainerResponse, error) {
|
|
start := time.Now()
|
|
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{
|
|
ContainerId: r.GetContainerId(),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("an error occurred when trying to find container the container status %q: %w", r.GetContainerId(), err)
|
|
}
|
|
|
|
container, err := c.containerStore.Get(r.GetContainerId())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("an error occurred when trying to find container %q: %w", r.GetContainerId(), err)
|
|
}
|
|
|
|
state := container.Status.Get().State()
|
|
if state != runtime.ContainerState_CONTAINER_RUNNING {
|
|
return nil, fmt.Errorf(
|
|
"container %q is in %s state. only %s containers can be checkpointed",
|
|
container.ID,
|
|
criContainerStateToString(state),
|
|
criContainerStateToString(runtime.ContainerState_CONTAINER_RUNNING),
|
|
)
|
|
}
|
|
|
|
i, err := container.Container.Info(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get container info: %w", err)
|
|
}
|
|
|
|
configJSON, err := json.Marshal(&crmetadata.ContainerConfig{
|
|
ID: container.ID,
|
|
Name: container.Name,
|
|
RootfsImageName: criContainerStatus.GetStatus().GetImage().GetImage(),
|
|
RootfsImageRef: criContainerStatus.GetStatus().GetImageRef(),
|
|
OCIRuntime: i.Runtime.Name,
|
|
RootfsImage: criContainerStatus.GetStatus().GetImage().GetImage(),
|
|
CheckpointedAt: time.Now(),
|
|
CreatedTime: i.CreatedAt,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generating container config JSON failed: %w", err)
|
|
}
|
|
|
|
task, err := container.Container.Task(ctx, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get task for container %q: %w", container.ID, err)
|
|
}
|
|
|
|
cpPath := filepath.Join(c.getContainerRootDir(container.ID), "ctrd-checkpoint")
|
|
// ctrd-checkpoint may already exist from a prior checkpoint operation. RemoveAll
|
|
// unlinks any existing entry (including a symlink) itself rather than its target,
|
|
// so creating the directory afterwards cannot write through a link.
|
|
if err := os.RemoveAll(cpPath); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.Mkdir(cpPath, 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.RemoveAll(cpPath)
|
|
|
|
// Point CRIU's work directory (where it writes dump.log and stats-dump) at the
|
|
// dedicated, freshly-created checkpoint dir instead of the persistent container
|
|
// state dir. Otherwise checkpoint creation litters those files into the state
|
|
// dir where they are never cleaned up; here they land directly where they are
|
|
// archived from and are removed with cpPath.
|
|
img, err := task.Checkpoint(ctx, []client.CheckpointTaskOpts{withCheckpointOpts(i.Runtime.Name, cpPath)}...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("checkpointing container %q failed: %w", container.ID, err)
|
|
}
|
|
|
|
// the checkpoint image has been provided as an index with manifests representing the tar of criu data, the rw layer, and the config
|
|
var (
|
|
index v1.Index
|
|
rawIndex []byte
|
|
targetDesc = img.Target()
|
|
contentStore = img.ContentStore()
|
|
)
|
|
|
|
// Once all content from the checkpoint image has been saved, the
|
|
// checkpoint image can be remove from the local image store.
|
|
defer c.client.ImageService().Delete(ctx, img.Metadata().Name)
|
|
|
|
rawIndex, err = content.ReadBlob(ctx, contentStore, targetDesc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve checkpoint index blob from content store: %w", err)
|
|
}
|
|
if err = json.Unmarshal(rawIndex, &index); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshall blob into checkpoint data OCI index: %w", err)
|
|
}
|
|
|
|
// This internal containerd file is used by checkpointctl for checkpoint archive
|
|
// analysis. It lives in the container state dir, which can hold files from a
|
|
// prior checkpoint operation, so it is read without following symlinks.
|
|
if err := copyNoFollow(
|
|
filepath.Join(c.getContainerRootDir(container.ID), crmetadata.StatusFile),
|
|
filepath.Join(cpPath, crmetadata.StatusFile),
|
|
0o600,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// dump.log and stats-dump are written directly into cpPath by CRIU via its
|
|
// work directory (see withCheckpointOpts above), so they are already present
|
|
// for archiving and do not need to be copied out of the container state dir.
|
|
|
|
// Save the existing container log file
|
|
_, err = c.os.Stat(criContainerStatus.GetStatus().GetLogPath())
|
|
if err == nil {
|
|
if err := c.os.CopyFile(
|
|
criContainerStatus.GetStatus().GetLogPath(),
|
|
filepath.Join(cpPath, "container.log"),
|
|
0o600,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(cpPath, crmetadata.ConfigDumpFile), configJSON, 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
containerStatus, err := json.Marshal(criContainerStatus.GetStatus())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal container status: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(cpPath, crmetadata.StatusDumpFile), containerStatus, 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// walk the manifests and pull out the blobs that we need to save in the checkpoint tarball:
|
|
// - the checkpoint criu data
|
|
// - the rw diff tarball
|
|
// - the spec blob
|
|
for _, manifest := range index.Manifests {
|
|
switch manifest.MediaType {
|
|
case images.MediaTypeContainerd1Checkpoint:
|
|
if err := writeCriuCheckpointData(ctx, contentStore, manifest, cpPath); err != nil {
|
|
return nil, fmt.Errorf("failed to copy CRIU checkpoint blob to checkpoint dir: %w", err)
|
|
}
|
|
case v1.MediaTypeImageLayerGzip:
|
|
if err := writeRootFsDiffTar(ctx, contentStore, manifest, cpPath); err != nil {
|
|
return nil, fmt.Errorf("failed to copy rw filesystem layer blob to checkpoint dir: %w", err)
|
|
}
|
|
case images.MediaTypeContainerd1CheckpointConfig:
|
|
if err := writeSpecDumpFile(ctx, contentStore, manifest, cpPath); err != nil {
|
|
return nil, fmt.Errorf("failed to copy container spec blob to checkpoint dir: %w", err)
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
|
|
// write final tarball of all content
|
|
tar := archive.Diff(ctx, "", cpPath)
|
|
|
|
outFile, err := os.OpenFile(r.Location, os.O_RDWR|os.O_CREATE, 0o600)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer outFile.Close()
|
|
_, err = io.Copy(outFile, tar)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tar.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
containerCheckpointTimer.WithValues(i.Runtime.Name).UpdateSince(start)
|
|
|
|
log.G(ctx).Infof("Wrote checkpoint archive to %s for %s", outFile.Name(), container.ID)
|
|
|
|
return &runtime.CheckpointContainerResponse{}, nil
|
|
}
|
|
|
|
func withCheckpointOpts(rt, rootDir string) client.CheckpointTaskOpts {
|
|
return func(r *client.CheckpointTaskInfo) error {
|
|
// Kubernetes currently supports checkpointing of container
|
|
// as part of the Forensic Container Checkpointing KEP.
|
|
// This implies that the container is never stopped
|
|
leaveRunning := true
|
|
|
|
switch rt {
|
|
case plugins.RuntimeRuncV2:
|
|
if r.Options == nil {
|
|
r.Options = &options.CheckpointOptions{}
|
|
}
|
|
opts, _ := r.Options.(*options.CheckpointOptions)
|
|
|
|
opts.Exit = !leaveRunning
|
|
opts.WorkPath = rootDir
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func writeCriuCheckpointData(ctx context.Context, store content.Store, desc v1.Descriptor, cpPath string) error {
|
|
ra, err := store.ReaderAt(ctx, desc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ra.Close()
|
|
|
|
checkpointDirectory := filepath.Join(cpPath, crmetadata.CheckpointDirectory)
|
|
// This is the criu data tarball. Let's unpack it
|
|
// and put it into the crmetadata.CheckpointDirectory directory.
|
|
if err := os.MkdirAll(checkpointDirectory, 0o700); err != nil {
|
|
return err
|
|
}
|
|
tr := tar.NewReader(content.NewReader(ra))
|
|
for {
|
|
header, err := tr.Next()
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
if strings.Contains(header.Name, "..") {
|
|
return fmt.Errorf("found illegal string '..' in checkpoint archive")
|
|
}
|
|
destFile, err := os.Create(filepath.Join(checkpointDirectory, header.Name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
_, err = io.CopyN(destFile, tr, header.Size)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeRootFsDiffTar(ctx context.Context, store content.Store, desc v1.Descriptor, cpPath string) error {
|
|
ra, err := store.ReaderAt(ctx, desc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ra.Close()
|
|
|
|
// the rw layer tarball
|
|
f, err := os.Create(filepath.Join(cpPath, crmetadata.RootFsDiffTar))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = io.Copy(f, content.NewReader(ra))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func writeSpecDumpFile(ctx context.Context, store content.Store, desc v1.Descriptor, cpPath string) error {
|
|
// this is the container spec
|
|
f, err := os.Create(filepath.Join(cpPath, crmetadata.SpecDumpFile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
data, err := content.ReadBlob(ctx, store, desc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var any ptypes.Any
|
|
if err := proto.Unmarshal(data, &any); err != nil {
|
|
return err
|
|
}
|
|
_, err = f.Write(any.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|