summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/containers/checkpoint.go4
-rw-r--r--cmd/podman/containers/restore.go72
-rw-r--r--docs/source/markdown/podman-container-checkpoint.1.md59
-rw-r--r--docs/source/markdown/podman-container-restore.1.md20
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--libpod/container_api.go6
-rw-r--r--libpod/container_internal.go5
-rw-r--r--libpod/container_internal_linux.go202
-rw-r--r--libpod/define/annotations.go70
-rw-r--r--pkg/api/handlers/libpod/containers.go33
-rw-r--r--pkg/bindings/containers/types.go1
-rw-r--r--pkg/bindings/containers/types_checkpoint_options.go15
-rw-r--r--pkg/checkpoint/checkpoint_restore.go11
-rw-r--r--pkg/checkpoint/crutils/checkpoint_restore_utils.go2
-rw-r--r--pkg/criu/criu.go5
-rw-r--r--pkg/domain/entities/containers.go2
-rw-r--r--pkg/domain/infra/abi/containers.go55
-rw-r--r--pkg/domain/infra/tunnel/containers.go39
-rw-r--r--test/e2e/checkpoint_image_test.go296
-rw-r--r--vendor/github.com/checkpoint-restore/checkpointctl/lib/metadata.go17
-rw-r--r--vendor/modules.txt2
22 files changed, 843 insertions, 79 deletions
diff --git a/cmd/podman/containers/checkpoint.go b/cmd/podman/containers/checkpoint.go
index f24e77106..40d689c4d 100644
--- a/cmd/podman/containers/checkpoint.go
+++ b/cmd/podman/containers/checkpoint.go
@@ -68,6 +68,10 @@ func init() {
flags.BoolVarP(&checkpointOptions.PreCheckPoint, "pre-checkpoint", "P", false, "Dump container's memory information only, leave the container running")
flags.BoolVar(&checkpointOptions.WithPrevious, "with-previous", false, "Checkpoint container with pre-checkpoint images")
+ createImageFlagName := "create-image"
+ flags.StringVarP(&checkpointOptions.CreateImage, createImageFlagName, "", "", "Create checkpoint image with specified name")
+ _ = checkpointCommand.RegisterFlagCompletionFunc(createImageFlagName, completion.AutocompleteNone)
+
flags.StringP("compress", "c", "zstd", "Select compression algorithm (gzip, none, zstd) for checkpoint archive.")
_ = checkpointCommand.RegisterFlagCompletionFunc("compress", common.AutocompleteCheckpointCompressType)
diff --git a/cmd/podman/containers/restore.go b/cmd/podman/containers/restore.go
index 3b51f5f17..eeda5a05f 100644
--- a/cmd/podman/containers/restore.go
+++ b/cmd/podman/containers/restore.go
@@ -10,9 +10,9 @@ import (
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/cmd/podman/validate"
+ "github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/containers/podman/v4/pkg/rootless"
- "github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -23,15 +23,16 @@ var (
Restores a container from a checkpoint. The container name or ID can be used.
`
restoreCommand = &cobra.Command{
- Use: "restore [options] CONTAINER [CONTAINER...]",
+ Use: "restore [options] CONTAINER|IMAGE [CONTAINER|IMAGE...]",
Short: "Restores one or more containers from a checkpoint",
Long: restoreDescription,
RunE: restore,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndCIDFile(cmd, args, true, false)
},
- ValidArgsFunction: common.AutocompleteContainers,
+ ValidArgsFunction: common.AutocompleteContainersAndImages,
Example: `podman container restore ctrID
+ podman container restore imageID
podman container restore --latest
podman container restore --all`,
}
@@ -60,7 +61,7 @@ func init() {
_ = restoreCommand.RegisterFlagCompletionFunc(importFlagName, completion.AutocompleteDefault)
nameFlagName := "name"
- flags.StringVarP(&restoreOptions.Name, nameFlagName, "n", "", "Specify new name for container restored from exported checkpoint (only works with --import)")
+ flags.StringVarP(&restoreOptions.Name, nameFlagName, "n", "", "Specify new name for container restored from exported checkpoint (only works with image or --import)")
_ = restoreCommand.RegisterFlagCompletionFunc(nameFlagName, completion.AutocompleteNone)
importPreviousFlagName := "import-previous"
@@ -78,7 +79,7 @@ func init() {
)
_ = restoreCommand.RegisterFlagCompletionFunc("publish", completion.AutocompleteNone)
- flags.StringVar(&restoreOptions.Pod, "pod", "", "Restore container into existing Pod (only works with --import)")
+ flags.StringVar(&restoreOptions.Pod, "pod", "", "Restore container into existing Pod (only works with image or --import)")
_ = restoreCommand.RegisterFlagCompletionFunc("pod", common.AutocompletePodsRunning)
flags.BoolVar(
@@ -95,25 +96,51 @@ func restore(cmd *cobra.Command, args []string) error {
var errs utils.OutputErrors
podmanStart := time.Now()
if rootless.IsRootless() {
- return errors.New("restoring a container requires root")
+ return fmt.Errorf("restoring a container requires root")
}
- if restoreOptions.Import == "" && restoreOptions.ImportPrevious != "" {
- return errors.Errorf("--import-previous can only be used with --import")
+
+ // Find out if this is an image
+ inspectOpts := entities.InspectOptions{}
+ imgData, _, err := registry.ImageEngine().Inspect(context.Background(), args, inspectOpts)
+ if err != nil {
+ return err
}
- if restoreOptions.Import == "" && restoreOptions.IgnoreRootFS {
- return errors.Errorf("--ignore-rootfs can only be used with --import")
+
+ hostInfo, err := registry.ContainerEngine().Info(context.Background())
+ if err != nil {
+ return err
}
- if restoreOptions.Import == "" && restoreOptions.IgnoreVolumes {
- return errors.Errorf("--ignore-volumes can only be used with --import")
+
+ for i := range imgData {
+ restoreOptions.CheckpointImage = true
+ checkpointRuntimeName, found := imgData[i].Annotations[define.CheckpointAnnotationRuntimeName]
+ if !found {
+ return fmt.Errorf("image is not a checkpoint: %s", imgData[i].ID)
+ }
+ if hostInfo.Host.OCIRuntime.Name != checkpointRuntimeName {
+ return fmt.Errorf("container image \"%s\" requires runtime: \"%s\"", imgData[i].ID, checkpointRuntimeName)
+ }
+ }
+
+ notImport := (!restoreOptions.CheckpointImage && restoreOptions.Import == "")
+
+ if notImport && restoreOptions.ImportPrevious != "" {
+ return fmt.Errorf("--import-previous can only be used with image or --import")
}
- if restoreOptions.Import == "" && restoreOptions.Name != "" {
- return errors.Errorf("--name can only be used with --import")
+ if notImport && restoreOptions.IgnoreRootFS {
+ return fmt.Errorf("--ignore-rootfs can only be used with image or --import")
}
- if restoreOptions.Import == "" && restoreOptions.Pod != "" {
- return errors.Errorf("--pod can only be used with --import")
+ if notImport && restoreOptions.IgnoreVolumes {
+ return fmt.Errorf("--ignore-volumes can only be used with image or --import")
+ }
+ if notImport && restoreOptions.Name != "" {
+ return fmt.Errorf("--name can only be used with image or --import")
+ }
+ if notImport && restoreOptions.Pod != "" {
+ return fmt.Errorf("--pod can only be used with image or --import")
}
if restoreOptions.Name != "" && restoreOptions.TCPEstablished {
- return errors.Errorf("--tcp-established cannot be used with --name")
+ return fmt.Errorf("--tcp-established cannot be used with --name")
}
inputPorts, err := cmd.Flags().GetStringSlice("publish")
@@ -125,17 +152,20 @@ func restore(cmd *cobra.Command, args []string) error {
argLen := len(args)
if restoreOptions.Import != "" {
if restoreOptions.All || restoreOptions.Latest {
- return errors.Errorf("Cannot use --import with --all or --latest")
+ return fmt.Errorf("cannot use --import with --all or --latest")
}
if argLen > 0 {
- return errors.Errorf("Cannot use --import with positional arguments")
+ return fmt.Errorf("cannot use --import with positional arguments")
}
}
if (restoreOptions.All || restoreOptions.Latest) && argLen > 0 {
- return errors.Errorf("--all or --latest and containers cannot be used together")
+ return fmt.Errorf("--all or --latest and containers cannot be used together")
}
if argLen < 1 && !restoreOptions.All && !restoreOptions.Latest && restoreOptions.Import == "" {
- return errors.Errorf("you must provide at least one name or id")
+ return fmt.Errorf("you must provide at least one name or id")
+ }
+ if argLen > 1 && restoreOptions.Name != "" {
+ return fmt.Errorf("--name can only be used with one checkpoint image")
}
responses, err := registry.ContainerEngine().ContainerRestore(context.Background(), args, restoreOptions)
if err != nil {
diff --git a/docs/source/markdown/podman-container-checkpoint.1.md b/docs/source/markdown/podman-container-checkpoint.1.md
index 5c07cd975..a11897081 100644
--- a/docs/source/markdown/podman-container-checkpoint.1.md
+++ b/docs/source/markdown/podman-container-checkpoint.1.md
@@ -28,6 +28,60 @@ archives. Not compressing the checkpoint archive can result in faster checkpoint
archive creation.\
The default is **zstd**.
+#### **--create-image**=*image*
+
+Create a checkpoint image from a running container. This is a standard OCI image
+created in the local image store. It consists of a single layer that contains
+all of the checkpoint files. The content of this image layer is in the same format as a
+checkpoint created with **--export**. A checkpoint image can be pushed to a
+standard container registry and pulled on a different system to enable container
+migration. In addition, the image can be exported with **podman image save** and
+inspected with **podman inspect**. Inspecting a checkpoint image would display
+additional information, stored as annotations, about the host environment used
+to do the checkpoint:
+
+- **io.podman.annotations.checkpoint.name**: Human-readable name of the original
+ container.
+
+- **io.podman.annotations.checkpoint.rawImageName**: Unprocessed name of the
+ image used to create the original container (as specified by the user).
+
+- **io.podman.annotations.checkpoint.rootfsImageID**: ID of the image used to
+ create the original container.
+
+- **io.podman.annotations.checkpoint.rootfsImageName**: Image name used to
+ create the original container.
+
+- **io.podman.annotations.checkpoint.podman.version**: Version of Podman used to
+ create the checkpoint.
+
+- **io.podman.annotations.checkpoint.criu.version**: Version of CRIU used to
+ create the checkpoint.
+
+- **io.podman.annotations.checkpoint.runtime.name**: Container runtime (e.g.,
+ runc, crun) used to create the checkpoint.
+
+- **io.podman.annotations.checkpoint.runtime.version**: Version of the container
+ runtime used to create the checkpoint.
+
+- **io.podman.annotations.checkpoint.conmon.version**: Version of conmon used
+ with the original container.
+
+- **io.podman.annotations.checkpoint.host.arch**: CPU architecture of the host
+ on which the checkpoint was created.
+
+- **io.podman.annotations.checkpoint.host.kernel**: Version of Linux kernel
+ of the host where the checkpoint was created.
+
+- **io.podman.annotations.checkpoint.cgroups.version**: cgroup version used by
+ the host where the checkpoint was created.
+
+- **io.podman.annotations.checkpoint.distribution.version**: Version of host
+ distribution on which the checkpoint was created.
+
+- **io.podman.annotations.checkpoint.distribution.name**: Name of host
+ distribution on which the checkpoint was created.
+
#### **--export**, **-e**=*archive*
Export the checkpoint to a tar.gz file. The exported checkpoint can be used
@@ -145,6 +199,11 @@ Make a checkpoint for the container "mywebserver".
# podman container checkpoint mywebserver
```
+Create a checkpoint image for the container "mywebserver".
+```
+# podman container checkpoint --create-image mywebserver-checkpoint-1 mywebserver
+```
+
Dumps the container's memory information of the latest container into an archive.
```
# podman container checkpoint -P -e pre-checkpoint.tar.gz -l
diff --git a/docs/source/markdown/podman-container-restore.1.md b/docs/source/markdown/podman-container-restore.1.md
index 5b1bf82c5..a70cc30d1 100644
--- a/docs/source/markdown/podman-container-restore.1.md
+++ b/docs/source/markdown/podman-container-restore.1.md
@@ -4,10 +4,11 @@
podman\-container\-restore - Restores one or more containers from a checkpoint
## SYNOPSIS
-**podman container restore** [*options*] *container* [*container* ...]
+**podman container restore** [*options*] *name* [...]
## DESCRIPTION
-**podman container restore** restores a container from a checkpoint. The *container IDs* or *names* are used as input.
+**podman container restore** restores a container from a container checkpoint or
+checkpoint image. The *container IDs*, *image IDs* or *names* are used as input.
## OPTIONS
#### **--all**, **-a**
@@ -106,14 +107,16 @@ If the **--name, -n** option is used, Podman will not attempt to assign the same
address to the *container* it was using before checkpointing as each IP address can only
be used once and the restored *container* will have another IP address. This also means
that **--name, -n** cannot be used in combination with **--tcp-established**.\
-*IMPORTANT: This OPTION is only available in combination with __--import, -i__.*
+*IMPORTANT: This OPTION is only available for a checkpoint image or in combination
+with __--import, -i__.*
#### **--pod**=*name*
Restore a container into the pod *name*. The destination pod for this restore
has to have the same namespaces shared as the pod this container was checkpointed
from (see **[podman pod create --share](podman-pod-create.1.md#--share)**).\
-*IMPORTANT: This OPTION is only available in combination with __--import, -i__.*
+*IMPORTANT: This OPTION is only available for a checkpoint image or in combination
+with __--import, -i__.*
This option requires at least CRIU 3.16.
@@ -175,6 +178,15 @@ $ podman run --rm -p 2345:80 -d webserver
# podman container restore -p 5432:8080 --import=dump.tar
```
+Start a container with the name "foobar-1". Create a checkpoint image "foobar-checkpoint". Restore the container from the checkpoint image with a different name.
+```
+# podman run --name foobar-1 -d webserver
+# podman container checkpoint --create-image foobar-checkpoint foobar-1
+# podman inspect foobar-checkpoint
+# podman container restore --name foobar-2 foobar-checkpoint
+# podman container restore --name foobar-3 foobar-checkpoint
+```
+
## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-container-checkpoint(1)](podman-container-checkpoint.1.md)**, **[podman-run(1)](podman-run.1.md)**, **[podman-pod-create(1)](podman-pod-create.1.md)**, **criu(8)**
diff --git a/go.mod b/go.mod
index b4e57ccc8..fcc1949fa 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v1.1.0
github.com/blang/semver v3.5.1+incompatible
github.com/buger/goterm v1.0.4
- github.com/checkpoint-restore/checkpointctl v0.0.0-20211204171957-54b4ebfdb681
+ github.com/checkpoint-restore/checkpointctl v0.0.0-20220321135231-33f4a66335f0
github.com/checkpoint-restore/go-criu/v5 v5.3.0
github.com/container-orchestrated-devices/container-device-interface v0.3.2
github.com/containernetworking/cni v1.0.1
diff --git a/go.sum b/go.sum
index 9268fbffe..a2a119dc4 100644
--- a/go.sum
+++ b/go.sum
@@ -208,8 +208,8 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg=
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU=
-github.com/checkpoint-restore/checkpointctl v0.0.0-20211204171957-54b4ebfdb681 h1:Jj8mYL2K6peLJdvT10oGTyYyBPqOynmly37D+iL3xNw=
-github.com/checkpoint-restore/checkpointctl v0.0.0-20211204171957-54b4ebfdb681/go.mod h1:67kWC1PXQLR3lM/mmNnu3Kzn7K4TSWZAGUuQP1JSngk=
+github.com/checkpoint-restore/checkpointctl v0.0.0-20220321135231-33f4a66335f0 h1:txB5jvhzUCSiiQmqmMWpo5CEB7Gj/Hq5Xqi7eaPl8ko=
+github.com/checkpoint-restore/checkpointctl v0.0.0-20220321135231-33f4a66335f0/go.mod h1:67kWC1PXQLR3lM/mmNnu3Kzn7K4TSWZAGUuQP1JSngk=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.2.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
diff --git a/libpod/container_api.go b/libpod/container_api.go
index 0b6139335..ebefed600 100644
--- a/libpod/container_api.go
+++ b/libpod/container_api.go
@@ -754,6 +754,9 @@ type ContainerCheckpointOptions struct {
// TargetFile tells the API to read (or write) the checkpoint image
// from (or to) the filename set in TargetFile
TargetFile string
+ // CheckpointImageID tells the API to restore the container from
+ // checkpoint image with ID set in CheckpointImageID
+ CheckpointImageID string
// Name tells the API that during restore from an exported
// checkpoint archive a new name should be used for the
// restored container
@@ -781,6 +784,9 @@ type ContainerCheckpointOptions struct {
// ImportPrevious tells the API to restore container with two
// images. One is TargetFile, the other is ImportPrevious.
ImportPrevious string
+ // CreateImage tells Podman to create an OCI image from container
+ // checkpoint in the local image store.
+ CreateImage string
// Compression tells the API which compression to use for
// the exported checkpoint archive.
Compression archive.Compression
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index c7567a55e..b051b7f2d 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -132,6 +132,11 @@ func (c *Container) ControlSocketPath() string {
return filepath.Join(c.bundlePath(), "ctl")
}
+// CheckpointVolumesPath returns the path to the directory containing the checkpointed volumes
+func (c *Container) CheckpointVolumesPath() string {
+ return filepath.Join(c.bundlePath(), metadata.CheckpointVolumesDirectory)
+}
+
// CheckpointPath returns the path to the directory containing the checkpoint
func (c *Container) CheckpointPath() string {
return filepath.Join(c.bundlePath(), metadata.CheckpointDirectory)
diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go
index 9991003d6..f8d0a124c 100644
--- a/libpod/container_internal_linux.go
+++ b/libpod/container_internal_linux.go
@@ -24,6 +24,7 @@ import (
"github.com/checkpoint-restore/go-criu/v5/stats"
cdi "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
"github.com/containernetworking/plugins/pkg/ns"
+ "github.com/containers/buildah"
"github.com/containers/buildah/pkg/chrootuser"
"github.com/containers/buildah/pkg/overlay"
butil "github.com/containers/buildah/util"
@@ -34,6 +35,7 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/subscriptions"
"github.com/containers/common/pkg/umask"
+ is "github.com/containers/image/v5/storage"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/annotations"
@@ -1098,6 +1100,124 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
return nil
}
+func (c *Container) addCheckpointImageMetadata(importBuilder *buildah.Builder) error {
+ // Get information about host environment
+ hostInfo, err := c.Runtime().hostInfo()
+ if err != nil {
+ return fmt.Errorf("getting host info: %v", err)
+ }
+
+ criuVersion, err := criu.GetCriuVestion()
+ if err != nil {
+ return fmt.Errorf("getting criu version: %v", err)
+ }
+
+ rootfsImageID, rootfsImageName := c.Image()
+
+ // Add image annotations with information about the container and the host.
+ // This information is useful to check compatibility before restoring the checkpoint
+
+ checkpointImageAnnotations := map[string]string{
+ define.CheckpointAnnotationName: c.config.Name,
+ define.CheckpointAnnotationRawImageName: c.config.RawImageName,
+ define.CheckpointAnnotationRootfsImageID: rootfsImageID,
+ define.CheckpointAnnotationRootfsImageName: rootfsImageName,
+ define.CheckpointAnnotationPodmanVersion: version.Version.String(),
+ define.CheckpointAnnotationCriuVersion: strconv.Itoa(criuVersion),
+ define.CheckpointAnnotationRuntimeName: hostInfo.OCIRuntime.Name,
+ define.CheckpointAnnotationRuntimeVersion: hostInfo.OCIRuntime.Version,
+ define.CheckpointAnnotationConmonVersion: hostInfo.Conmon.Version,
+ define.CheckpointAnnotationHostArch: hostInfo.Arch,
+ define.CheckpointAnnotationHostKernel: hostInfo.Kernel,
+ define.CheckpointAnnotationCgroupVersion: hostInfo.CgroupsVersion,
+ define.CheckpointAnnotationDistributionVersion: hostInfo.Distribution.Version,
+ define.CheckpointAnnotationDistributionName: hostInfo.Distribution.Distribution,
+ }
+
+ for key, value := range checkpointImageAnnotations {
+ importBuilder.SetAnnotation(key, value)
+ }
+
+ return nil
+}
+
+func (c *Container) resolveCheckpointImageName(options *ContainerCheckpointOptions) error {
+ if options.CreateImage == "" {
+ return nil
+ }
+
+ // Resolve image name
+ resolvedImageName, err := c.runtime.LibimageRuntime().ResolveName(options.CreateImage)
+ if err != nil {
+ return err
+ }
+
+ options.CreateImage = resolvedImageName
+ return nil
+}
+
+func (c *Container) createCheckpointImage(ctx context.Context, options ContainerCheckpointOptions) error {
+ if options.CreateImage == "" {
+ return nil
+ }
+ logrus.Debugf("Create checkpoint image %s", options.CreateImage)
+
+ // Create storage reference
+ imageRef, err := is.Transport.ParseStoreReference(c.runtime.store, options.CreateImage)
+ if err != nil {
+ return errors.Errorf("Failed to parse image name")
+ }
+
+ // Build an image scratch
+ builderOptions := buildah.BuilderOptions{
+ FromImage: "scratch",
+ }
+ importBuilder, err := buildah.NewBuilder(ctx, c.runtime.store, builderOptions)
+ if err != nil {
+ return err
+ }
+ // Clean-up buildah working container
+ defer importBuilder.Delete()
+
+ if err := c.prepareCheckpointExport(); err != nil {
+ return err
+ }
+
+ // Export checkpoint into temporary tar file
+ tmpDir, err := ioutil.TempDir("", "checkpoint_image_")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(tmpDir)
+
+ options.TargetFile = path.Join(tmpDir, "checkpoint.tar")
+
+ if err := c.exportCheckpoint(options); err != nil {
+ return err
+ }
+
+ // Copy checkpoint from temporary tar file in the image
+ addAndCopyOptions := buildah.AddAndCopyOptions{}
+ importBuilder.Add("", true, addAndCopyOptions, options.TargetFile)
+
+ if err := c.addCheckpointImageMetadata(importBuilder); err != nil {
+ return err
+ }
+
+ commitOptions := buildah.CommitOptions{
+ Squash: true,
+ SystemContext: c.runtime.imageContext,
+ }
+
+ // Create checkpoint image
+ id, _, _, err := importBuilder.Commit(ctx, imageRef, commitOptions)
+ if err != nil {
+ return err
+ }
+ logrus.Debugf("Created checkpoint image: %s", id)
+ return nil
+}
+
func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {
if len(c.Dependencies()) == 1 {
// Check if the dependency is an infra container. If it is we can checkpoint
@@ -1159,7 +1279,7 @@ func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {
}
// Folder containing archived volumes that will be included in the export
- expVolDir := filepath.Join(c.bundlePath(), "volumes")
+ expVolDir := filepath.Join(c.bundlePath(), metadata.CheckpointVolumesDirectory)
// Create an archive for each volume associated with the container
if !options.IgnoreVolumes {
@@ -1168,7 +1288,7 @@ func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {
}
for _, v := range c.config.NamedVolumes {
- volumeTarFilePath := filepath.Join("volumes", v.Name+".tar")
+ volumeTarFilePath := filepath.Join(metadata.CheckpointVolumesDirectory, v.Name+".tar")
volumeTarFileFullPath := filepath.Join(c.bundlePath(), volumeTarFilePath)
volumeTarFile, err := os.Create(volumeTarFileFullPath)
@@ -1266,6 +1386,10 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
return nil, 0, errors.Errorf("cannot checkpoint containers that have been started with '--rm' unless '--export' is used")
}
+ if err := c.resolveCheckpointImageName(&options); err != nil {
+ return nil, 0, err
+ }
+
if err := crutils.CRCreateFileWithLabel(c.bundlePath(), "dump.log", c.MountLabel()); err != nil {
return nil, 0, err
}
@@ -1325,6 +1449,10 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
if err := c.exportCheckpoint(options); err != nil {
return nil, 0, err
}
+ } else {
+ if err := c.createCheckpointImage(ctx, options); err != nil {
+ return nil, 0, err
+ }
}
logrus.Debugf("Checkpointed container %s", c.ID())
@@ -1390,11 +1518,7 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
return criuStatistics, runtimeCheckpointDuration, c.save()
}
-func (c *Container) importCheckpoint(input string) error {
- if err := crutils.CRImportCheckpointWithoutConfig(c.bundlePath(), input); err != nil {
- return err
- }
-
+func (c *Container) generateContainerSpec() error {
// Make sure the newly created config.json exists on disk
g := generate.NewFromSpec(c.config.Spec)
@@ -1405,6 +1529,51 @@ func (c *Container) importCheckpoint(input string) error {
return nil
}
+func (c *Container) importCheckpointImage(ctx context.Context, imageID string) error {
+ img, _, err := c.Runtime().LibimageRuntime().LookupImage(imageID, nil)
+ if err != nil {
+ return err
+ }
+
+ mountPoint, err := img.Mount(ctx, nil, "")
+ defer img.Unmount(true)
+ if err != nil {
+ return err
+ }
+
+ // Import all checkpoint files except ConfigDumpFile and SpecDumpFile. We
+ // generate new container config files to enable to specifying a new
+ // container name.
+ checkpoint := []string{
+ "artifacts",
+ metadata.CheckpointDirectory,
+ metadata.CheckpointVolumesDirectory,
+ metadata.DevShmCheckpointTar,
+ metadata.RootFsDiffTar,
+ metadata.DeletedFilesFile,
+ metadata.PodOptionsFile,
+ metadata.PodDumpFile,
+ }
+
+ for _, name := range checkpoint {
+ src := filepath.Join(mountPoint, name)
+ dst := filepath.Join(c.bundlePath(), name)
+ if err := archive.NewDefaultArchiver().CopyWithTar(src, dst); err != nil {
+ logrus.Debugf("Can't import '%s' from checkpoint image", name)
+ }
+ }
+
+ return c.generateContainerSpec()
+}
+
+func (c *Container) importCheckpointTar(input string) error {
+ if err := crutils.CRImportCheckpointWithoutConfig(c.bundlePath(), input); err != nil {
+ return err
+ }
+
+ return c.generateContainerSpec()
+}
+
func (c *Container) importPreCheckpoint(input string) error {
archiveFile, err := os.Open(input)
if err != nil {
@@ -1446,7 +1615,11 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
}
if options.TargetFile != "" {
- if err := c.importCheckpoint(options.TargetFile); err != nil {
+ if err := c.importCheckpointTar(options.TargetFile); err != nil {
+ return nil, 0, err
+ }
+ } else if options.CheckpointImageID != "" {
+ if err := c.importCheckpointImage(ctx, options.CheckpointImageID); err != nil {
return nil, 0, err
}
}
@@ -1531,7 +1704,7 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
}
// Restoring from an import means that we are doing migration
- if options.TargetFile != "" {
+ if options.TargetFile != "" || options.CheckpointImageID != "" {
g.SetRootPath(c.state.Mountpoint)
}
@@ -1628,7 +1801,7 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
return nil, 0, err
}
- if options.TargetFile != "" {
+ if options.TargetFile != "" || options.CheckpointImageID != "" {
for dstPath, srcPath := range c.state.BindMounts {
newMount := spec.Mount{
Type: "bind",
@@ -1678,9 +1851,9 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
// When restoring from an imported archive, allow restoring the content of volumes.
// Volumes are created in setupContainer()
- if options.TargetFile != "" && !options.IgnoreVolumes {
+ if !options.IgnoreVolumes && (options.TargetFile != "" || options.CheckpointImageID != "") {
for _, v := range c.config.NamedVolumes {
- volumeFilePath := filepath.Join(c.bundlePath(), "volumes", v.Name+".tar")
+ volumeFilePath := filepath.Join(c.bundlePath(), metadata.CheckpointVolumesDirectory, v.Name+".tar")
volumeFile, err := os.Open(volumeFilePath)
if err != nil {
@@ -1770,11 +1943,16 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
if err != nil {
logrus.Debugf("Non-fatal: removal of pre-checkpoint directory (%s) failed: %v", c.PreCheckPointPath(), err)
}
+ err = os.RemoveAll(c.CheckpointVolumesPath())
+ if err != nil {
+ logrus.Debugf("Non-fatal: removal of checkpoint volumes directory (%s) failed: %v", c.CheckpointVolumesPath(), err)
+ }
cleanup := [...]string{
"restore.log",
"dump.log",
stats.StatsDump,
stats.StatsRestore,
+ metadata.DevShmCheckpointTar,
metadata.NetworkStatusFile,
metadata.RootFsDiffTar,
metadata.DeletedFilesFile,
diff --git a/libpod/define/annotations.go b/libpod/define/annotations.go
index 3964a1237..a83fbff0b 100644
--- a/libpod/define/annotations.go
+++ b/libpod/define/annotations.go
@@ -65,6 +65,76 @@ const (
// InspectResponseFalse is a boolean False response for an inspect
// annotation.
InspectResponseFalse = "FALSE"
+
+ // CheckpointAnnotationName is used by Container Checkpoint when creating a
+ // checkpoint image to specify the original human-readable name for the
+ // container.
+ CheckpointAnnotationName = "io.podman.annotations.checkpoint.name"
+
+ // CheckpointAnnotationRawImageName is used by Container Checkpoint when
+ // creating a checkpoint image to specify the original unprocessed name of
+ // the image used to create the container (as specified by the user).
+ CheckpointAnnotationRawImageName = "io.podman.annotations.checkpoint.rawImageName"
+
+ // CheckpointAnnotationRootfsImageID is used by Container Checkpoint when
+ // creating a checkpoint image to specify the original ID of the image used
+ // to create the container.
+ CheckpointAnnotationRootfsImageID = "io.podman.annotations.checkpoint.rootfsImageID"
+
+ // CheckpointAnnotationRootfsImageName is used by Container Checkpoint when
+ // creating a checkpoint image to specify the original image name used to
+ // create the container.
+ CheckpointAnnotationRootfsImageName = "io.podman.annotations.checkpoint.rootfsImageName"
+
+ // CheckpointAnnotationPodmanVersion is used by Container Checkpoint when
+ // creating a checkpoint image to specify the version of Podman used on the
+ // host where the checkpoint was created.
+ CheckpointAnnotationPodmanVersion = "io.podman.annotations.checkpoint.podman.version"
+
+ // CheckpointAnnotationCriuVersion is used by Container Checkpoint when
+ // creating a checkpoint image to specify the version of CRIU used on the
+ // host where the checkpoint was created.
+ CheckpointAnnotationCriuVersion = "io.podman.annotations.checkpoint.criu.version"
+
+ // CheckpointAnnotationRuntimeName is used by Container Checkpoint when
+ // creating a checkpoint image to specify the runtime used on the host where
+ // the checkpoint was created.
+ CheckpointAnnotationRuntimeName = "io.podman.annotations.checkpoint.runtime.name"
+
+ // CheckpointAnnotationRuntimeVersion is used by Container Checkpoint when
+ // creating a checkpoint image to specify the version of runtime used on the
+ // host where the checkpoint was created.
+ CheckpointAnnotationRuntimeVersion = "io.podman.annotations.checkpoint.runtime.version"
+
+ // CheckpointAnnotationConmonVersion is used by Container Checkpoint when
+ // creating a checkpoint image to specify the version of conmon used on
+ // the host where the checkpoint was created.
+ CheckpointAnnotationConmonVersion = "io.podman.annotations.checkpoint.conmon.version"
+
+ // CheckpointAnnotationHostArch is used by Container Checkpoint when
+ // creating a checkpoint image to specify the CPU architecture of the host
+ // on which the checkpoint was created.
+ CheckpointAnnotationHostArch = "io.podman.annotations.checkpoint.host.arch"
+
+ // CheckpointAnnotationHostKernel is used by Container Checkpoint when
+ // creating a checkpoint image to specify the kernel version used by the
+ // host where the checkpoint was created.
+ CheckpointAnnotationHostKernel = "io.podman.annotations.checkpoint.host.kernel"
+
+ // CheckpointAnnotationCgroupVersion is used by Container Checkpoint when
+ // creating a checkpoint image to specify the cgroup version used by the
+ // host where the checkpoint was created.
+ CheckpointAnnotationCgroupVersion = "io.podman.annotations.checkpoint.cgroups.version"
+
+ // CheckpointAnnotationDistributionVersion is used by Container Checkpoint
+ // when creating a checkpoint image to specify the version of host
+ // distribution on which the checkpoint was created.
+ CheckpointAnnotationDistributionVersion = "io.podman.annotations.checkpoint.distribution.version"
+
+ // CheckpointAnnotationDistributionName is used by Container Checkpoint when
+ // creating a checkpoint image to specify the name of host distribution on
+ // which the checkpoint was created.
+ CheckpointAnnotationDistributionName = "io.podman.annotations.checkpoint.distribution.name"
)
// IsReservedAnnotation returns true if the specified value corresponds to an
diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go
index dfa09b8b8..03dd436f6 100644
--- a/pkg/api/handlers/libpod/containers.go
+++ b/pkg/api/handlers/libpod/containers.go
@@ -209,15 +209,16 @@ func Checkpoint(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
query := struct {
- Keep bool `schema:"keep"`
- LeaveRunning bool `schema:"leaveRunning"`
- TCPEstablished bool `schema:"tcpEstablished"`
- Export bool `schema:"export"`
- IgnoreRootFS bool `schema:"ignoreRootFS"`
- PrintStats bool `schema:"printStats"`
- PreCheckpoint bool `schema:"preCheckpoint"`
- WithPrevious bool `schema:"withPrevious"`
- FileLocks bool `schema:"fileLocks"`
+ Keep bool `schema:"keep"`
+ LeaveRunning bool `schema:"leaveRunning"`
+ TCPEstablished bool `schema:"tcpEstablished"`
+ Export bool `schema:"export"`
+ IgnoreRootFS bool `schema:"ignoreRootFS"`
+ PrintStats bool `schema:"printStats"`
+ PreCheckpoint bool `schema:"preCheckpoint"`
+ WithPrevious bool `schema:"withPrevious"`
+ FileLocks bool `schema:"fileLocks"`
+ CreateImage string `schema:"createImage"`
}{
// override any golang type defaults
}
@@ -243,6 +244,7 @@ func Checkpoint(w http.ResponseWriter, r *http.Request) {
PreCheckPoint: query.PreCheckpoint,
WithPrevious: query.WithPrevious,
FileLocks: query.FileLocks,
+ CreateImage: query.CreateImage,
}
if query.Export {
@@ -341,8 +343,17 @@ func Restore(w http.ResponseWriter, r *http.Request) {
} else {
name := utils.GetName(r)
if _, err := runtime.LookupContainer(name); err != nil {
- utils.ContainerNotFound(w, name, err)
- return
+ // If container was not found, check if this is a checkpoint image
+ ir := abi.ImageEngine{Libpod: runtime}
+ report, err := ir.Exists(r.Context(), name)
+ if err != nil {
+ utils.Error(w, http.StatusNotFound, errors.Wrapf(err, "failed to find container or checkpoint image %s", name))
+ return
+ }
+ if !report.Value {
+ utils.Error(w, http.StatusNotFound, errors.Errorf("failed to find container or checkpoint image %s", name))
+ return
+ }
}
names = []string{name}
}
diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go
index c87f82bf4..81d491bb7 100644
--- a/pkg/bindings/containers/types.go
+++ b/pkg/bindings/containers/types.go
@@ -47,6 +47,7 @@ type AttachOptions struct {
// CheckpointOptions are optional options for checkpointing containers
type CheckpointOptions struct {
Export *string
+ CreateImage *string
IgnoreRootfs *bool
Keep *bool
LeaveRunning *bool
diff --git a/pkg/bindings/containers/types_checkpoint_options.go b/pkg/bindings/containers/types_checkpoint_options.go
index e717daf9f..d5f6e541d 100644
--- a/pkg/bindings/containers/types_checkpoint_options.go
+++ b/pkg/bindings/containers/types_checkpoint_options.go
@@ -32,6 +32,21 @@ func (o *CheckpointOptions) GetExport() string {
return *o.Export
}
+// WithCreateImage set field CreateImage to given value
+func (o *CheckpointOptions) WithCreateImage(value string) *CheckpointOptions {
+ o.CreateImage = &value
+ return o
+}
+
+// GetCreateImage returns value of field CreateImage
+func (o *CheckpointOptions) GetCreateImage() string {
+ if o.CreateImage == nil {
+ var z string
+ return z
+ }
+ return *o.CreateImage
+}
+
// WithIgnoreRootfs set field IgnoreRootfs to given value
func (o *CheckpointOptions) WithIgnoreRootfs(value bool) *CheckpointOptions {
o.IgnoreRootfs = &value
diff --git a/pkg/checkpoint/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go
index 270b5b6c4..396b521a1 100644
--- a/pkg/checkpoint/checkpoint_restore.go
+++ b/pkg/checkpoint/checkpoint_restore.go
@@ -22,9 +22,7 @@ import (
// Prefixing the checkpoint/restore related functions with 'cr'
-// CRImportCheckpoint it the function which imports the information
-// from checkpoint tarball and re-creates the container from that information
-func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions) ([]*libpod.Container, error) {
+func CRImportCheckpointTar(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions) ([]*libpod.Container, error) {
// First get the container definition from the
// tarball to a temporary directory
dir, err := ioutil.TempDir("", "checkpoint")
@@ -39,7 +37,12 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt
if err := crutils.CRImportCheckpointConfigOnly(dir, restoreOptions.Import); err != nil {
return nil, err
}
+ return CRImportCheckpoint(ctx, runtime, restoreOptions, dir)
+}
+// CRImportCheckpoint it the function which imports the information
+// from checkpoint tarball and re-creates the container from that information
+func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions, dir string) ([]*libpod.Container, error) {
// Load spec.dump from temporary directory
dumpSpec := new(spec.Spec)
if _, err := metadata.ReadJSONFile(dumpSpec, dir, metadata.SpecDumpFile); err != nil {
@@ -48,7 +51,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt
// Load config.dump from temporary directory
ctrConfig := new(libpod.ContainerConfig)
- if _, err = metadata.ReadJSONFile(ctrConfig, dir, metadata.ConfigDumpFile); err != nil {
+ if _, err := metadata.ReadJSONFile(ctrConfig, dir, metadata.ConfigDumpFile); err != nil {
return nil, err
}
diff --git a/pkg/checkpoint/crutils/checkpoint_restore_utils.go b/pkg/checkpoint/crutils/checkpoint_restore_utils.go
index 6a8a7894a..76c868cee 100644
--- a/pkg/checkpoint/crutils/checkpoint_restore_utils.go
+++ b/pkg/checkpoint/crutils/checkpoint_restore_utils.go
@@ -54,7 +54,6 @@ func CRImportCheckpointConfigOnly(destination, input string) error {
options := &archive.TarOptions{
// Here we only need the files config.dump and spec.dump
ExcludePatterns: []string{
- "volumes",
"ctr.log",
"artifacts",
stats.StatsDump,
@@ -62,6 +61,7 @@ func CRImportCheckpointConfigOnly(destination, input string) error {
metadata.DeletedFilesFile,
metadata.NetworkStatusFile,
metadata.CheckpointDirectory,
+ metadata.CheckpointVolumesDirectory,
},
}
if err = archive.Untar(archiveFile, destination, options); err != nil {
diff --git a/pkg/criu/criu.go b/pkg/criu/criu.go
index b54870abc..6570159d7 100644
--- a/pkg/criu/criu.go
+++ b/pkg/criu/criu.go
@@ -28,6 +28,11 @@ func CheckForCriu(version int) bool {
return result
}
+func GetCriuVestion() (int, error) {
+ c := criu.MakeCriu()
+ return c.GetCriuVersion()
+}
+
func MemTrack() bool {
features, err := criu.MakeCriu().FeatureCheck(
&rpc.CriuFeatures{
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index 072514d0f..3d1d7a6d2 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -178,6 +178,7 @@ type ContainerExportOptions struct {
type CheckpointOptions struct {
All bool
Export string
+ CreateImage string
IgnoreRootFS bool
IgnoreVolumes bool
Keep bool
@@ -205,6 +206,7 @@ type RestoreOptions struct {
IgnoreStaticIP bool
IgnoreStaticMAC bool
Import string
+ CheckpointImage bool
Keep bool
Latest bool
Name string
diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go
index 100842c69..46ef01b80 100644
--- a/pkg/domain/infra/abi/containers.go
+++ b/pkg/domain/infra/abi/containers.go
@@ -563,6 +563,7 @@ func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds [
Compression: options.Compression,
PrintStats: options.PrintStats,
FileLocks: options.FileLocks,
+ CreateImage: options.CreateImage,
}
if options.All {
@@ -592,8 +593,9 @@ func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds [
func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) {
var (
- cons []*libpod.Container
- err error
+ containers []*libpod.Container
+ checkpointImageImportErrors []error
+ err error
)
restoreOptions := libpod.ContainerCheckpointOptions{
@@ -619,17 +621,49 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
switch {
case options.Import != "":
- cons, err = checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options)
+ containers, err = checkpoint.CRImportCheckpointTar(ctx, ic.Libpod, options)
case options.All:
- cons, err = ic.Libpod.GetContainers(filterFuncs...)
+ containers, err = ic.Libpod.GetContainers(filterFuncs...)
+ case options.Latest:
+ containers, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod)
default:
- cons, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod)
+ for _, nameOrID := range namesOrIds {
+ logrus.Debugf("lookup container: %q", nameOrID)
+ ctr, err := ic.Libpod.LookupContainer(nameOrID)
+ if err == nil {
+ containers = append(containers, ctr)
+ } else {
+ // If container was not found, check if this is a checkpoint image
+ logrus.Debugf("lookup image: %q", nameOrID)
+ img, _, err := ic.Libpod.LibimageRuntime().LookupImage(nameOrID, nil)
+ if err != nil {
+ return nil, fmt.Errorf("no such container or image: %s", nameOrID)
+ }
+ restoreOptions.CheckpointImageID = img.ID()
+ mountPoint, err := img.Mount(ctx, nil, "")
+ defer img.Unmount(true)
+ if err != nil {
+ return nil, err
+ }
+ importedContainers, err := checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options, mountPoint)
+ if err != nil {
+ // CRImportCheckpoint is expected to import exactly one container from checkpoint image
+ checkpointImageImportErrors = append(
+ checkpointImageImportErrors,
+ errors.Errorf("unable to import checkpoint from image: %q: %v", nameOrID, err),
+ )
+ } else {
+ containers = append(containers, importedContainers[0])
+ }
+ }
+ }
}
if err != nil {
return nil, err
}
- reports := make([]*entities.RestoreReport, 0, len(cons))
- for _, con := range cons {
+
+ reports := make([]*entities.RestoreReport, 0, len(containers))
+ for _, con := range containers {
criuStatistics, runtimeRestoreDuration, err := con.Restore(ctx, restoreOptions)
reports = append(reports, &entities.RestoreReport{
Err: err,
@@ -638,6 +672,13 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
CRIUStatistics: criuStatistics,
})
}
+
+ for _, importErr := range checkpointImageImportErrors {
+ reports = append(reports, &entities.RestoreReport{
+ Err: importErr,
+ })
+ }
+
return reports, nil
}
diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go
index 10bfb3984..82e8fbb5b 100644
--- a/pkg/domain/infra/tunnel/containers.go
+++ b/pkg/domain/infra/tunnel/containers.go
@@ -16,6 +16,7 @@ import (
"github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/api/handlers"
"github.com/containers/podman/v4/pkg/bindings/containers"
+ "github.com/containers/podman/v4/pkg/bindings/images"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/containers/podman/v4/pkg/domain/entities/reports"
"github.com/containers/podman/v4/pkg/errorhandling"
@@ -331,6 +332,7 @@ func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds [
options.WithIgnoreRootfs(opts.IgnoreRootFS)
options.WithKeep(opts.Keep)
options.WithExport(opts.Export)
+ options.WithCreateImage(opts.CreateImage)
options.WithTCPEstablished(opts.TCPEstablished)
options.WithPrintStats(opts.PrintStats)
options.WithPreCheckpoint(opts.PreCheckPoint)
@@ -396,8 +398,7 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
}
var (
- err error
- ctrs = []entities.ListContainer{}
+ ids = []string{}
)
if opts.All {
allCtrs, err := getContainersByContext(ic.ClientCtx, true, false, []string{})
@@ -407,20 +408,42 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
// narrow the list to exited only
for _, c := range allCtrs {
if c.State == define.ContainerStateExited.String() {
- ctrs = append(ctrs, c)
+ ids = append(ids, c.ID)
}
}
} else {
- ctrs, err = getContainersByContext(ic.ClientCtx, false, false, namesOrIds)
+ getImageOptions := new(images.GetOptions).WithSize(false)
+ hostInfo, err := ic.Info(context.Background())
if err != nil {
return nil, err
}
+
+ for _, nameOrID := range namesOrIds {
+ ctrData, _, err := ic.ContainerInspect(ic.ClientCtx, []string{nameOrID}, entities.InspectOptions{})
+ if err == nil && len(ctrData) > 0 {
+ ids = append(ids, ctrData[0].ID)
+ } else {
+ // If container was not found, check if this is a checkpoint image
+ inspectReport, err := images.GetImage(ic.ClientCtx, nameOrID, getImageOptions)
+ if err != nil {
+ return nil, fmt.Errorf("no such container or image: %s", nameOrID)
+ }
+ checkpointRuntimeName, found := inspectReport.Annotations[define.CheckpointAnnotationRuntimeName]
+ if !found {
+ return nil, fmt.Errorf("image is not a checkpoint: %s", nameOrID)
+ }
+ if hostInfo.Host.OCIRuntime.Name != checkpointRuntimeName {
+ return nil, fmt.Errorf("container image \"%s\" requires runtime: \"%s\"", nameOrID, checkpointRuntimeName)
+ }
+ ids = append(ids, inspectReport.ID)
+ }
+ }
}
- reports := make([]*entities.RestoreReport, 0, len(ctrs))
- for _, c := range ctrs {
- report, err := containers.Restore(ic.ClientCtx, c.ID, options)
+ reports := make([]*entities.RestoreReport, 0, len(ids))
+ for _, id := range ids {
+ report, err := containers.Restore(ic.ClientCtx, id, options)
if err != nil {
- reports = append(reports, &entities.RestoreReport{Id: c.ID, Err: err})
+ reports = append(reports, &entities.RestoreReport{Id: id, Err: err})
}
reports = append(reports, report)
}
diff --git a/test/e2e/checkpoint_image_test.go b/test/e2e/checkpoint_image_test.go
new file mode 100644
index 000000000..8274dfc80
--- /dev/null
+++ b/test/e2e/checkpoint_image_test.go
@@ -0,0 +1,296 @@
+package integration
+
+import (
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+
+ "github.com/containers/podman/v4/pkg/criu"
+ . "github.com/containers/podman/v4/test/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ . "github.com/onsi/gomega/gexec"
+)
+
+var _ = Describe("Podman checkpoint", func() {
+ var (
+ tempdir string
+ err error
+ podmanTest *PodmanTestIntegration
+ )
+
+ BeforeEach(func() {
+ SkipIfRootless("checkpoint not supported in rootless mode")
+ tempdir, err = CreateTempDirInTempDir()
+ if err != nil {
+ os.Exit(1)
+ }
+ podmanTest = PodmanTestCreate(tempdir)
+ podmanTest.Setup()
+ podmanTest.SeedImages()
+ // Check if the runtime implements checkpointing. Currently only
+ // runc's checkpoint/restore implementation is supported.
+ cmd := exec.Command(podmanTest.OCIRuntime, "checkpoint", "--help")
+ if err := cmd.Start(); err != nil {
+ Skip("OCI runtime does not support checkpoint/restore")
+ }
+ if err := cmd.Wait(); err != nil {
+ Skip("OCI runtime does not support checkpoint/restore")
+ }
+
+ if !criu.CheckForCriu(criu.MinCriuVersion) {
+ Skip("CRIU is missing or too old.")
+ }
+ })
+
+ AfterEach(func() {
+ podmanTest.Cleanup()
+ f := CurrentGinkgoTestDescription()
+ processTestResult(f)
+ })
+
+ It("podman checkpoint --create-image with bogus container", func() {
+ checkpointImage := "foobar-checkpoint"
+ session := podmanTest.Podman([]string{"container", "checkpoint", "--create-image", checkpointImage, "foobar"})
+ session.WaitWithDefaultTimeout()
+ Expect(session).To(ExitWithError())
+ Expect(session.ErrorToString()).To(ContainSubstring("no container with name or ID \"foobar\" found"))
+ })
+
+ It("podman checkpoint --create-image with running container", func() {
+ // Container image must be lowercase
+ checkpointImage := "alpine-checkpoint-" + strings.ToLower(RandomString(6))
+ containerName := "alpine-container-" + RandomString(6)
+
+ localRunString := []string{
+ "run",
+ "-it",
+ "-d",
+ "--ip", GetRandomIPAddress(),
+ "--name", containerName,
+ ALPINE,
+ "top",
+ }
+ session := podmanTest.Podman(localRunString)
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ containerID := session.OutputToString()
+
+ // Checkpoint image should not exist
+ session = podmanTest.Podman([]string{"images"})
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ Expect(session.LineInOutputContainsTag("localhost/"+checkpointImage, "latest")).To(BeFalse())
+
+ // Check if none of the checkpoint/restore specific information is displayed
+ // for newly started containers.
+ inspect := podmanTest.Podman([]string{"inspect", containerID})
+ inspect.WaitWithDefaultTimeout()
+ Expect(inspect).Should(Exit(0))
+ inspectOut := inspect.InspectContainerToJSON()
+ Expect(inspectOut[0].State.Checkpointed).To(BeFalse(), ".State.Checkpointed")
+ Expect(inspectOut[0].State.Restored).To(BeFalse(), ".State.Restored")
+ Expect(inspectOut[0].State.CheckpointPath).To(Equal(""))
+ Expect(inspectOut[0].State.CheckpointLog).To(Equal(""))
+ Expect(inspectOut[0].State.RestoreLog).To(Equal(""))
+
+ result := podmanTest.Podman([]string{"container", "checkpoint", "--create-image", checkpointImage, "--keep", containerID})
+ result.WaitWithDefaultTimeout()
+
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+ Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited"))
+
+ inspect = podmanTest.Podman([]string{"inspect", containerID})
+ inspect.WaitWithDefaultTimeout()
+ Expect(inspect).Should(Exit(0))
+ inspectOut = inspect.InspectContainerToJSON()
+ Expect(inspectOut[0].State.Checkpointed).To(BeTrue(), ".State.Checkpointed")
+ Expect(inspectOut[0].State.CheckpointPath).To(ContainSubstring("userdata/checkpoint"))
+ Expect(inspectOut[0].State.CheckpointLog).To(ContainSubstring("userdata/dump.log"))
+
+ // Check if checkpoint image has been created
+ session = podmanTest.Podman([]string{"images"})
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ Expect(session.LineInOutputContainsTag("localhost/"+checkpointImage, "latest")).To(BeTrue())
+
+ // Check if the checkpoint image contains annotations
+ inspect = podmanTest.Podman([]string{"inspect", checkpointImage})
+ inspect.WaitWithDefaultTimeout()
+ Expect(inspect).Should(Exit(0))
+ inspectImageOut := inspect.InspectImageJSON()
+ Expect(inspectImageOut[0].Annotations["io.podman.annotations.checkpoint.name"]).To(
+ BeEquivalentTo(containerName),
+ "io.podman.annotations.checkpoint.name",
+ )
+
+ ociRuntimeName := ""
+ if strings.Contains(podmanTest.OCIRuntime, "runc") {
+ ociRuntimeName = "runc"
+ } else if strings.Contains(podmanTest.OCIRuntime, "crun") {
+ ociRuntimeName = "crun"
+ }
+ if ociRuntimeName != "" {
+ Expect(inspectImageOut[0].Annotations["io.podman.annotations.checkpoint.runtime.name"]).To(
+ BeEquivalentTo(ociRuntimeName),
+ "io.podman.annotations.checkpoint.runtime.name",
+ )
+ }
+
+ // Remove existing container
+ result = podmanTest.Podman([]string{"rm", "-t", "1", "-f", containerID})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+
+ // Restore container from checkpoint image
+ result = podmanTest.Podman([]string{"container", "restore", checkpointImage})
+ result.WaitWithDefaultTimeout()
+
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1))
+ Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Up"))
+
+ // Clean-up
+ result = podmanTest.Podman([]string{"rm", "-t", "0", "-fa"})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+
+ result = podmanTest.Podman([]string{"rmi", checkpointImage})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+ })
+
+ It("podman restore multiple containers from single checkpint image", func() {
+ // Container image must be lowercase
+ checkpointImage := "alpine-checkpoint-" + strings.ToLower(RandomString(6))
+ containerName := "alpine-container-" + RandomString(6)
+
+ localRunString := []string{"run", "-d", "--name", containerName, ALPINE, "top"}
+ session := podmanTest.Podman(localRunString)
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ containerID := session.OutputToString()
+
+ // Checkpoint image should not exist
+ session = podmanTest.Podman([]string{"images"})
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ Expect(session.LineInOutputContainsTag("localhost/"+checkpointImage, "latest")).To(BeFalse())
+
+ result := podmanTest.Podman([]string{"container", "checkpoint", "--create-image", checkpointImage, "--keep", containerID})
+ result.WaitWithDefaultTimeout()
+
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+ Expect(podmanTest.GetContainerStatus()).To(ContainSubstring("Exited"))
+
+ // Check if checkpoint image has been created
+ session = podmanTest.Podman([]string{"images"})
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ Expect(session.LineInOutputContainsTag("localhost/"+checkpointImage, "latest")).To(BeTrue())
+
+ // Remove existing container
+ result = podmanTest.Podman([]string{"rm", "-t", "1", "-f", containerID})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+
+ for i := 1; i < 5; i++ {
+ // Restore container from checkpoint image
+ name := containerName + strconv.Itoa(i)
+ result = podmanTest.Podman([]string{"container", "restore", "--name", name, checkpointImage})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(i))
+
+ // Check that the container is running
+ status := podmanTest.Podman([]string{"inspect", name, "--format={{.State.Status}}"})
+ status.WaitWithDefaultTimeout()
+ Expect(status).Should(Exit(0))
+ Expect(status.OutputToString()).To(Equal("running"))
+ }
+
+ // Clean-up
+ result = podmanTest.Podman([]string{"rm", "-t", "0", "-fa"})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+
+ result = podmanTest.Podman([]string{"rmi", checkpointImage})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+ })
+
+ It("podman restore multiple containers from multiple checkpint images", func() {
+ // Container image must be lowercase
+ checkpointImage1 := "alpine-checkpoint-" + strings.ToLower(RandomString(6))
+ checkpointImage2 := "alpine-checkpoint-" + strings.ToLower(RandomString(6))
+ containerName1 := "alpine-container-" + RandomString(6)
+ containerName2 := "alpine-container-" + RandomString(6)
+
+ // Create first container
+ localRunString := []string{"run", "-d", "--name", containerName1, ALPINE, "top"}
+ session := podmanTest.Podman(localRunString)
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ containerID1 := session.OutputToString()
+
+ // Create second container
+ localRunString = []string{"run", "-d", "--name", containerName2, ALPINE, "top"}
+ session = podmanTest.Podman(localRunString)
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+ containerID2 := session.OutputToString()
+
+ // Checkpoint first container
+ result := podmanTest.Podman([]string{"container", "checkpoint", "--create-image", checkpointImage1, "--keep", containerID1})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1))
+
+ // Checkpoint second container
+ result = podmanTest.Podman([]string{"container", "checkpoint", "--create-image", checkpointImage2, "--keep", containerID2})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+
+ // Remove existing containers
+ result = podmanTest.Podman([]string{"rm", "-t", "1", "-f", containerName1, containerName2})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+
+ // Restore both containers from images
+ result = podmanTest.Podman([]string{"container", "restore", checkpointImage1, checkpointImage2})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(2))
+
+ // Check if first container is running
+ status := podmanTest.Podman([]string{"inspect", containerName1, "--format={{.State.Status}}"})
+ status.WaitWithDefaultTimeout()
+ Expect(status).Should(Exit(0))
+ Expect(status.OutputToString()).To(Equal("running"))
+
+ // Check if second container is running
+ status = podmanTest.Podman([]string{"inspect", containerName2, "--format={{.State.Status}}"})
+ status.WaitWithDefaultTimeout()
+ Expect(status).Should(Exit(0))
+ Expect(status.OutputToString()).To(Equal("running"))
+
+ // Clean-up
+ result = podmanTest.Podman([]string{"rm", "-t", "0", "-fa"})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+
+ result = podmanTest.Podman([]string{"rmi", checkpointImage1, checkpointImage2})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
+ })
+})
diff --git a/vendor/github.com/checkpoint-restore/checkpointctl/lib/metadata.go b/vendor/github.com/checkpoint-restore/checkpointctl/lib/metadata.go
index 712fd2d50..3b1abd928 100644
--- a/vendor/github.com/checkpoint-restore/checkpointctl/lib/metadata.go
+++ b/vendor/github.com/checkpoint-restore/checkpointctl/lib/metadata.go
@@ -48,13 +48,16 @@ const (
// kubelet archive
CheckpointedPodsFile = "checkpointed.pods"
// container archive
- ConfigDumpFile = "config.dump"
- SpecDumpFile = "spec.dump"
- NetworkStatusFile = "network.status"
- CheckpointDirectory = "checkpoint"
- DevShmCheckpointTar = "devshm-checkpoint.tar"
- RootFsDiffTar = "rootfs-diff.tar"
- DeletedFilesFile = "deleted.files"
+ ConfigDumpFile = "config.dump"
+ SpecDumpFile = "spec.dump"
+ NetworkStatusFile = "network.status"
+ CheckpointDirectory = "checkpoint"
+ CheckpointVolumesDirectory = "volumes"
+ DevShmCheckpointTar = "devshm-checkpoint.tar"
+ RootFsDiffTar = "rootfs-diff.tar"
+ DeletedFilesFile = "deleted.files"
+ DumpLogFile = "dump.log"
+ RestoreLogFile = "restore.log"
// pod archive
PodOptionsFile = "pod.options"
PodDumpFile = "pod.dump"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 06e446ff7..8150ab970 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -46,7 +46,7 @@ github.com/blang/semver
github.com/buger/goterm
# github.com/cespare/xxhash/v2 v2.1.2
github.com/cespare/xxhash/v2
-# github.com/checkpoint-restore/checkpointctl v0.0.0-20211204171957-54b4ebfdb681
+# github.com/checkpoint-restore/checkpointctl v0.0.0-20220321135231-33f4a66335f0
## explicit
github.com/checkpoint-restore/checkpointctl/lib
# github.com/checkpoint-restore/go-criu/v5 v5.3.0