Add support for checkpoint image

This is an enhancement proposal for the checkpoint / restore feature of
Podman that enables container migration across multiple systems with
standard image distribution infrastructure.

A new option `--create-image <image>` has been added to the
`podman container checkpoint` command. This option tells Podman to
create a container image.  This is a standard image with a single layer,
tar archive, that that contains all checkpoint files. This is similar to
the current approach with checkpoint `--export`/`--import`.

This image can be pushed to a container registry and pulled on a
different system.  It can also be exported locally with `podman image
save` and inspected with `podman inspect`. Inspecting the image would
display additional information about the host and the versions of
Podman, criu, crun/runc, kernel, etc.

`podman container restore` has also been extended to support image
name or ID as input.

Suggested-by: Adrian Reber <areber@redhat.com>
Signed-off-by: Radostin Stoyanov <radostin@redhat.com>
This commit is contained in:
Radostin Stoyanov
2022-04-12 18:46:32 +01:00
parent fca3397dc9
commit 756ecd5400
17 changed files with 533 additions and 68 deletions

View File

@ -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.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") 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.") flags.StringP("compress", "c", "zstd", "Select compression algorithm (gzip, none, zstd) for checkpoint archive.")
_ = checkpointCommand.RegisterFlagCompletionFunc("compress", common.AutocompleteCheckpointCompressType) _ = checkpointCommand.RegisterFlagCompletionFunc("compress", common.AutocompleteCheckpointCompressType)

View File

@ -10,9 +10,9 @@ import (
"github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils" "github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/cmd/podman/validate" "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/domain/entities"
"github.com/containers/podman/v4/pkg/rootless" "github.com/containers/podman/v4/pkg/rootless"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -23,15 +23,16 @@ var (
Restores a container from a checkpoint. The container name or ID can be used. Restores a container from a checkpoint. The container name or ID can be used.
` `
restoreCommand = &cobra.Command{ restoreCommand = &cobra.Command{
Use: "restore [options] CONTAINER [CONTAINER...]", Use: "restore [options] CONTAINER|IMAGE [CONTAINER|IMAGE...]",
Short: "Restores one or more containers from a checkpoint", Short: "Restores one or more containers from a checkpoint",
Long: restoreDescription, Long: restoreDescription,
RunE: restore, RunE: restore,
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndCIDFile(cmd, args, true, false) return validate.CheckAllLatestAndCIDFile(cmd, args, true, false)
}, },
ValidArgsFunction: common.AutocompleteContainers, ValidArgsFunction: common.AutocompleteContainersAndImages,
Example: `podman container restore ctrID Example: `podman container restore ctrID
podman container restore imageID
podman container restore --latest podman container restore --latest
podman container restore --all`, podman container restore --all`,
} }
@ -60,7 +61,7 @@ func init() {
_ = restoreCommand.RegisterFlagCompletionFunc(importFlagName, completion.AutocompleteDefault) _ = restoreCommand.RegisterFlagCompletionFunc(importFlagName, completion.AutocompleteDefault)
nameFlagName := "name" 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) _ = restoreCommand.RegisterFlagCompletionFunc(nameFlagName, completion.AutocompleteNone)
importPreviousFlagName := "import-previous" importPreviousFlagName := "import-previous"
@ -78,7 +79,7 @@ func init() {
) )
_ = restoreCommand.RegisterFlagCompletionFunc("publish", completion.AutocompleteNone) _ = 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) _ = restoreCommand.RegisterFlagCompletionFunc("pod", common.AutocompletePodsRunning)
flags.BoolVar( flags.BoolVar(
@ -95,25 +96,51 @@ func restore(cmd *cobra.Command, args []string) error {
var errs utils.OutputErrors var errs utils.OutputErrors
podmanStart := time.Now() podmanStart := time.Now()
if rootless.IsRootless() { 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)
}
} }
if restoreOptions.Import == "" && restoreOptions.Name != "" {
return errors.Errorf("--name can only be used with --import") 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.Pod != "" { if notImport && restoreOptions.IgnoreRootFS {
return errors.Errorf("--pod can only be used with --import") return fmt.Errorf("--ignore-rootfs can only be used with image or --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 { 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") inputPorts, err := cmd.Flags().GetStringSlice("publish")
@ -125,17 +152,20 @@ func restore(cmd *cobra.Command, args []string) error {
argLen := len(args) argLen := len(args)
if restoreOptions.Import != "" { if restoreOptions.Import != "" {
if restoreOptions.All || restoreOptions.Latest { 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 { 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 { 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 == "" { 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) responses, err := registry.ContainerEngine().ContainerRestore(context.Background(), args, restoreOptions)
if err != nil { if err != nil {

View File

@ -28,6 +28,60 @@ archives. Not compressing the checkpoint archive can result in faster checkpoint
archive creation.\ archive creation.\
The default is **zstd**. 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**, **-e**=*archive*
Export the checkpoint to a tar.gz file. The exported checkpoint can be used 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 # 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. Dumps the container's memory information of the latest container into an archive.
``` ```
# podman container checkpoint -P -e pre-checkpoint.tar.gz -l # podman container checkpoint -P -e pre-checkpoint.tar.gz -l

View File

@ -4,10 +4,11 @@
podman\-container\-restore - Restores one or more containers from a checkpoint podman\-container\-restore - Restores one or more containers from a checkpoint
## SYNOPSIS ## SYNOPSIS
**podman container restore** [*options*] *container* [*container* ...] **podman container restore** [*options*] *name* [...]
## DESCRIPTION ## 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 ## OPTIONS
#### **--all**, **-a** #### **--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 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 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**.\ 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* #### **--pod**=*name*
Restore a container into the pod *name*. The destination pod for this restore 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 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)**).\ 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. 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 # 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 ## 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)** **[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)**

View File

@ -754,6 +754,9 @@ type ContainerCheckpointOptions struct {
// TargetFile tells the API to read (or write) the checkpoint image // TargetFile tells the API to read (or write) the checkpoint image
// from (or to) the filename set in TargetFile // from (or to) the filename set in TargetFile
TargetFile string 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 // Name tells the API that during restore from an exported
// checkpoint archive a new name should be used for the // checkpoint archive a new name should be used for the
// restored container // restored container
@ -781,6 +784,9 @@ type ContainerCheckpointOptions struct {
// ImportPrevious tells the API to restore container with two // ImportPrevious tells the API to restore container with two
// images. One is TargetFile, the other is ImportPrevious. // images. One is TargetFile, the other is ImportPrevious.
ImportPrevious string 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 // Compression tells the API which compression to use for
// the exported checkpoint archive. // the exported checkpoint archive.
Compression archive.Compression Compression archive.Compression

View File

@ -132,6 +132,11 @@ func (c *Container) ControlSocketPath() string {
return filepath.Join(c.bundlePath(), "ctl") 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 // CheckpointPath returns the path to the directory containing the checkpoint
func (c *Container) CheckpointPath() string { func (c *Container) CheckpointPath() string {
return filepath.Join(c.bundlePath(), metadata.CheckpointDirectory) return filepath.Join(c.bundlePath(), metadata.CheckpointDirectory)

View File

@ -24,6 +24,7 @@ import (
"github.com/checkpoint-restore/go-criu/v5/stats" "github.com/checkpoint-restore/go-criu/v5/stats"
cdi "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi" cdi "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
"github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/buildah"
"github.com/containers/buildah/pkg/chrootuser" "github.com/containers/buildah/pkg/chrootuser"
"github.com/containers/buildah/pkg/overlay" "github.com/containers/buildah/pkg/overlay"
butil "github.com/containers/buildah/util" butil "github.com/containers/buildah/util"
@ -34,6 +35,7 @@ import (
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/subscriptions" "github.com/containers/common/pkg/subscriptions"
"github.com/containers/common/pkg/umask" "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/define"
"github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/annotations" "github.com/containers/podman/v4/pkg/annotations"
@ -1098,6 +1100,124 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
return nil 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 { func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {
if len(c.Dependencies()) == 1 { if len(c.Dependencies()) == 1 {
// Check if the dependency is an infra container. If it is we can checkpoint // 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 // 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 // Create an archive for each volume associated with the container
if !options.IgnoreVolumes { if !options.IgnoreVolumes {
@ -1168,7 +1288,7 @@ func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {
} }
for _, v := range c.config.NamedVolumes { 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) volumeTarFileFullPath := filepath.Join(c.bundlePath(), volumeTarFilePath)
volumeTarFile, err := os.Create(volumeTarFileFullPath) 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") 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 { if err := crutils.CRCreateFileWithLabel(c.bundlePath(), "dump.log", c.MountLabel()); err != nil {
return nil, 0, err return nil, 0, err
} }
@ -1325,6 +1449,10 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
if err := c.exportCheckpoint(options); err != nil { if err := c.exportCheckpoint(options); err != nil {
return nil, 0, err return nil, 0, err
} }
} else {
if err := c.createCheckpointImage(ctx, options); err != nil {
return nil, 0, err
}
} }
logrus.Debugf("Checkpointed container %s", c.ID()) 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() return criuStatistics, runtimeCheckpointDuration, c.save()
} }
func (c *Container) importCheckpoint(input string) error { func (c *Container) generateContainerSpec() error {
if err := crutils.CRImportCheckpointWithoutConfig(c.bundlePath(), input); err != nil {
return err
}
// Make sure the newly created config.json exists on disk // Make sure the newly created config.json exists on disk
g := generate.NewFromSpec(c.config.Spec) g := generate.NewFromSpec(c.config.Spec)
@ -1405,6 +1529,51 @@ func (c *Container) importCheckpoint(input string) error {
return nil 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 { func (c *Container) importPreCheckpoint(input string) error {
archiveFile, err := os.Open(input) archiveFile, err := os.Open(input)
if err != nil { if err != nil {
@ -1446,7 +1615,11 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
} }
if options.TargetFile != "" { 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 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 // Restoring from an import means that we are doing migration
if options.TargetFile != "" { if options.TargetFile != "" || options.CheckpointImageID != "" {
g.SetRootPath(c.state.Mountpoint) g.SetRootPath(c.state.Mountpoint)
} }
@ -1628,7 +1801,7 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
return nil, 0, err return nil, 0, err
} }
if options.TargetFile != "" { if options.TargetFile != "" || options.CheckpointImageID != "" {
for dstPath, srcPath := range c.state.BindMounts { for dstPath, srcPath := range c.state.BindMounts {
newMount := spec.Mount{ newMount := spec.Mount{
Type: "bind", 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. // When restoring from an imported archive, allow restoring the content of volumes.
// Volumes are created in setupContainer() // Volumes are created in setupContainer()
if options.TargetFile != "" && !options.IgnoreVolumes { if !options.IgnoreVolumes && (options.TargetFile != "" || options.CheckpointImageID != "") {
for _, v := range c.config.NamedVolumes { 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) volumeFile, err := os.Open(volumeFilePath)
if err != nil { if err != nil {
@ -1770,11 +1943,16 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
if err != nil { if err != nil {
logrus.Debugf("Non-fatal: removal of pre-checkpoint directory (%s) failed: %v", c.PreCheckPointPath(), err) 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{ cleanup := [...]string{
"restore.log", "restore.log",
"dump.log", "dump.log",
stats.StatsDump, stats.StatsDump,
stats.StatsRestore, stats.StatsRestore,
metadata.DevShmCheckpointTar,
metadata.NetworkStatusFile, metadata.NetworkStatusFile,
metadata.RootFsDiffTar, metadata.RootFsDiffTar,
metadata.DeletedFilesFile, metadata.DeletedFilesFile,

View File

@ -65,6 +65,76 @@ const (
// InspectResponseFalse is a boolean False response for an inspect // InspectResponseFalse is a boolean False response for an inspect
// annotation. // annotation.
InspectResponseFalse = "FALSE" 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 // IsReservedAnnotation returns true if the specified value corresponds to an

View File

@ -209,15 +209,16 @@ func Checkpoint(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
query := struct { query := struct {
Keep bool `schema:"keep"` Keep bool `schema:"keep"`
LeaveRunning bool `schema:"leaveRunning"` LeaveRunning bool `schema:"leaveRunning"`
TCPEstablished bool `schema:"tcpEstablished"` TCPEstablished bool `schema:"tcpEstablished"`
Export bool `schema:"export"` Export bool `schema:"export"`
IgnoreRootFS bool `schema:"ignoreRootFS"` IgnoreRootFS bool `schema:"ignoreRootFS"`
PrintStats bool `schema:"printStats"` PrintStats bool `schema:"printStats"`
PreCheckpoint bool `schema:"preCheckpoint"` PreCheckpoint bool `schema:"preCheckpoint"`
WithPrevious bool `schema:"withPrevious"` WithPrevious bool `schema:"withPrevious"`
FileLocks bool `schema:"fileLocks"` FileLocks bool `schema:"fileLocks"`
CreateImage string `schema:"createImage"`
}{ }{
// override any golang type defaults // override any golang type defaults
} }
@ -243,6 +244,7 @@ func Checkpoint(w http.ResponseWriter, r *http.Request) {
PreCheckPoint: query.PreCheckpoint, PreCheckPoint: query.PreCheckpoint,
WithPrevious: query.WithPrevious, WithPrevious: query.WithPrevious,
FileLocks: query.FileLocks, FileLocks: query.FileLocks,
CreateImage: query.CreateImage,
} }
if query.Export { if query.Export {
@ -341,8 +343,17 @@ func Restore(w http.ResponseWriter, r *http.Request) {
} else { } else {
name := utils.GetName(r) name := utils.GetName(r)
if _, err := runtime.LookupContainer(name); err != nil { if _, err := runtime.LookupContainer(name); err != nil {
utils.ContainerNotFound(w, name, err) // If container was not found, check if this is a checkpoint image
return 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} names = []string{name}
} }

View File

@ -47,6 +47,7 @@ type AttachOptions struct {
// CheckpointOptions are optional options for checkpointing containers // CheckpointOptions are optional options for checkpointing containers
type CheckpointOptions struct { type CheckpointOptions struct {
Export *string Export *string
CreateImage *string
IgnoreRootfs *bool IgnoreRootfs *bool
Keep *bool Keep *bool
LeaveRunning *bool LeaveRunning *bool

View File

@ -32,6 +32,21 @@ func (o *CheckpointOptions) GetExport() string {
return *o.Export 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 // WithIgnoreRootfs set field IgnoreRootfs to given value
func (o *CheckpointOptions) WithIgnoreRootfs(value bool) *CheckpointOptions { func (o *CheckpointOptions) WithIgnoreRootfs(value bool) *CheckpointOptions {
o.IgnoreRootfs = &value o.IgnoreRootfs = &value

View File

@ -22,9 +22,7 @@ import (
// Prefixing the checkpoint/restore related functions with 'cr' // Prefixing the checkpoint/restore related functions with 'cr'
// CRImportCheckpoint it the function which imports the information func CRImportCheckpointTar(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions) ([]*libpod.Container, error) {
// 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) {
// First get the container definition from the // First get the container definition from the
// tarball to a temporary directory // tarball to a temporary directory
dir, err := ioutil.TempDir("", "checkpoint") 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 { if err := crutils.CRImportCheckpointConfigOnly(dir, restoreOptions.Import); err != nil {
return nil, err 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 // Load spec.dump from temporary directory
dumpSpec := new(spec.Spec) dumpSpec := new(spec.Spec)
if _, err := metadata.ReadJSONFile(dumpSpec, dir, metadata.SpecDumpFile); err != nil { 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 // Load config.dump from temporary directory
ctrConfig := new(libpod.ContainerConfig) 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 return nil, err
} }

View File

@ -54,7 +54,6 @@ func CRImportCheckpointConfigOnly(destination, input string) error {
options := &archive.TarOptions{ options := &archive.TarOptions{
// Here we only need the files config.dump and spec.dump // Here we only need the files config.dump and spec.dump
ExcludePatterns: []string{ ExcludePatterns: []string{
"volumes",
"ctr.log", "ctr.log",
"artifacts", "artifacts",
stats.StatsDump, stats.StatsDump,
@ -62,6 +61,7 @@ func CRImportCheckpointConfigOnly(destination, input string) error {
metadata.DeletedFilesFile, metadata.DeletedFilesFile,
metadata.NetworkStatusFile, metadata.NetworkStatusFile,
metadata.CheckpointDirectory, metadata.CheckpointDirectory,
metadata.CheckpointVolumesDirectory,
}, },
} }
if err = archive.Untar(archiveFile, destination, options); err != nil { if err = archive.Untar(archiveFile, destination, options); err != nil {

View File

@ -28,6 +28,11 @@ func CheckForCriu(version int) bool {
return result return result
} }
func GetCriuVestion() (int, error) {
c := criu.MakeCriu()
return c.GetCriuVersion()
}
func MemTrack() bool { func MemTrack() bool {
features, err := criu.MakeCriu().FeatureCheck( features, err := criu.MakeCriu().FeatureCheck(
&rpc.CriuFeatures{ &rpc.CriuFeatures{

View File

@ -178,6 +178,7 @@ type ContainerExportOptions struct {
type CheckpointOptions struct { type CheckpointOptions struct {
All bool All bool
Export string Export string
CreateImage string
IgnoreRootFS bool IgnoreRootFS bool
IgnoreVolumes bool IgnoreVolumes bool
Keep bool Keep bool
@ -205,6 +206,7 @@ type RestoreOptions struct {
IgnoreStaticIP bool IgnoreStaticIP bool
IgnoreStaticMAC bool IgnoreStaticMAC bool
Import string Import string
CheckpointImage bool
Keep bool Keep bool
Latest bool Latest bool
Name string Name string

View File

@ -563,6 +563,7 @@ func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds [
Compression: options.Compression, Compression: options.Compression,
PrintStats: options.PrintStats, PrintStats: options.PrintStats,
FileLocks: options.FileLocks, FileLocks: options.FileLocks,
CreateImage: options.CreateImage,
} }
if options.All { 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) { func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) {
var ( var (
cons []*libpod.Container containers []*libpod.Container
err error checkpointImageImportErrors []error
err error
) )
restoreOptions := libpod.ContainerCheckpointOptions{ restoreOptions := libpod.ContainerCheckpointOptions{
@ -619,17 +621,49 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
switch { switch {
case options.Import != "": case options.Import != "":
cons, err = checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options) containers, err = checkpoint.CRImportCheckpointTar(ctx, ic.Libpod, options)
case options.All: 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: 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 { if err != nil {
return nil, err 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) criuStatistics, runtimeRestoreDuration, err := con.Restore(ctx, restoreOptions)
reports = append(reports, &entities.RestoreReport{ reports = append(reports, &entities.RestoreReport{
Err: err, Err: err,
@ -638,6 +672,13 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
CRIUStatistics: criuStatistics, CRIUStatistics: criuStatistics,
}) })
} }
for _, importErr := range checkpointImageImportErrors {
reports = append(reports, &entities.RestoreReport{
Err: importErr,
})
}
return reports, nil return reports, nil
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/libpod/events"
"github.com/containers/podman/v4/pkg/api/handlers" "github.com/containers/podman/v4/pkg/api/handlers"
"github.com/containers/podman/v4/pkg/bindings/containers" "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"
"github.com/containers/podman/v4/pkg/domain/entities/reports" "github.com/containers/podman/v4/pkg/domain/entities/reports"
"github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/podman/v4/pkg/errorhandling"
@ -331,6 +332,7 @@ func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds [
options.WithIgnoreRootfs(opts.IgnoreRootFS) options.WithIgnoreRootfs(opts.IgnoreRootFS)
options.WithKeep(opts.Keep) options.WithKeep(opts.Keep)
options.WithExport(opts.Export) options.WithExport(opts.Export)
options.WithCreateImage(opts.CreateImage)
options.WithTCPEstablished(opts.TCPEstablished) options.WithTCPEstablished(opts.TCPEstablished)
options.WithPrintStats(opts.PrintStats) options.WithPrintStats(opts.PrintStats)
options.WithPreCheckpoint(opts.PreCheckPoint) options.WithPreCheckpoint(opts.PreCheckPoint)
@ -396,8 +398,7 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
} }
var ( var (
err error ids = []string{}
ctrs = []entities.ListContainer{}
) )
if opts.All { if opts.All {
allCtrs, err := getContainersByContext(ic.ClientCtx, true, false, []string{}) 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 // narrow the list to exited only
for _, c := range allCtrs { for _, c := range allCtrs {
if c.State == define.ContainerStateExited.String() { if c.State == define.ContainerStateExited.String() {
ctrs = append(ctrs, c) ids = append(ids, c.ID)
} }
} }
} else { } else {
ctrs, err = getContainersByContext(ic.ClientCtx, false, false, namesOrIds) getImageOptions := new(images.GetOptions).WithSize(false)
hostInfo, err := ic.Info(context.Background())
if err != nil { if err != nil {
return nil, err 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)) reports := make([]*entities.RestoreReport, 0, len(ids))
for _, c := range ctrs { for _, id := range ids {
report, err := containers.Restore(ic.ClientCtx, c.ID, options) report, err := containers.Restore(ic.ClientCtx, id, options)
if err != nil { 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) reports = append(reports, report)
} }