Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for checkpoint/restore of containers with volumes #8781

Merged
merged 4 commits into from
Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/podman/containers/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func init() {
_ = checkpointCommand.RegisterFlagCompletionFunc(exportFlagName, completion.AutocompleteDefault)

flags.BoolVar(&checkpointOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not include root file-system changes when exporting")
flags.BoolVar(&checkpointOptions.IgnoreVolumes, "ignore-volumes", false, "Do not export volumes associated with container")
validate.AddLatestFlag(checkpointCommand, &checkpointOptions.Latest)
}

Expand All @@ -68,6 +69,9 @@ func checkpoint(cmd *cobra.Command, args []string) error {
if checkpointOptions.Export == "" && checkpointOptions.IgnoreRootFS {
return errors.Errorf("--ignore-rootfs can only be used with --export")
}
if checkpointOptions.Export == "" && checkpointOptions.IgnoreVolumes {
return errors.Errorf("--ignore-volumes can only be used with --export")
}
responses, err := registry.ContainerEngine().ContainerCheckpoint(context.Background(), args, checkpointOptions)
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions cmd/podman/containers/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func init() {
flags.BoolVar(&restoreOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not apply root file-system changes when importing from exported checkpoint")
flags.BoolVar(&restoreOptions.IgnoreStaticIP, "ignore-static-ip", false, "Ignore IP address set via --static-ip")
flags.BoolVar(&restoreOptions.IgnoreStaticMAC, "ignore-static-mac", false, "Ignore MAC address set via --mac-address")
flags.BoolVar(&restoreOptions.IgnoreVolumes, "ignore-volumes", false, "Do not export volumes associated with container")
validate.AddLatestFlag(restoreCommand, &restoreOptions.Latest)
}

Expand All @@ -73,6 +74,9 @@ func restore(_ *cobra.Command, args []string) error {
if restoreOptions.Import == "" && restoreOptions.IgnoreRootFS {
return errors.Errorf("--ignore-rootfs can only be used with --import")
}
if restoreOptions.Import == "" && restoreOptions.IgnoreVolumes {
return errors.Errorf("--ignore-volumes can only be used with --import")
}
if restoreOptions.Import == "" && restoreOptions.Name != "" {
return errors.Errorf("--name can only be used with --import")
}
Expand Down
6 changes: 6 additions & 0 deletions docs/source/markdown/podman-container-checkpoint.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ exported to a tar.gz file it is possible with the help of **--ignore-rootfs**
to explicitly disable including changes to the root file-system into
the checkpoint archive file.

#### **--ignore-volumes**

This option must be used in combination with the **--export, -e** option.
When this option is specified, the content of volumes associated with
the container will not be included into the checkpoint tar.gz file.

## EXAMPLE

podman container checkpoint mywebserver
Expand Down
7 changes: 7 additions & 0 deletions docs/source/markdown/podman-container-restore.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ exported checkpoint with **--name, -n**.

Using **--ignore-static-mac** tells Podman to ignore the MAC address if it was
configured with **--mac-address** during container creation.

#### **--ignore-volumes**

This option must be used in combination with the **--import, -i** option.
When restoring containers from a checkpoint tar.gz file with this option,
the content of associated volumes will not be restored.

## EXAMPLE

podman container restore mywebserver
Expand Down
3 changes: 3 additions & 0 deletions libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,9 @@ type ContainerCheckpointOptions struct {
// important to be able to restore a container multiple
// times with '--import --name'.
IgnoreStaticMAC bool
// IgnoreVolumes tells the API to not export or not to import
// the content of volumes associated with the container
IgnoreVolumes bool
}

// Checkpoint checkpoints a container
Expand Down
87 changes: 78 additions & 9 deletions libpod/container_internal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -790,11 +790,11 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
return nil
}

func (c *Container) exportCheckpoint(dest string, ignoreRootfs bool) error {
if (len(c.config.NamedVolumes) > 0) || (len(c.Dependencies()) > 0) {
return errors.Errorf("Cannot export checkpoints of containers with named volumes or dependencies")
func (c *Container) exportCheckpoint(options ContainerCheckpointOptions) error {
if len(c.Dependencies()) > 0 {
return errors.Errorf("Cannot export checkpoints of containers with dependencies")
}
logrus.Debugf("Exporting checkpoint image of container %q to %q", c.ID(), dest)
logrus.Debugf("Exporting checkpoint image of container %q to %q", c.ID(), options.TargetFile)

includeFiles := []string{
"checkpoint",
Expand All @@ -807,7 +807,7 @@ func (c *Container) exportCheckpoint(dest string, ignoreRootfs bool) error {
// Get root file-system changes included in the checkpoint archive
rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar")
deleteFilesList := filepath.Join(c.bundlePath(), "deleted.files")
if !ignoreRootfs {
if !options.IgnoreRootfs {
// To correctly track deleted files, let's go through the output of 'podman diff'
tarFiles, err := c.runtime.GetDiff("", c.ID())
if err != nil {
Expand Down Expand Up @@ -870,6 +870,47 @@ func (c *Container) exportCheckpoint(dest string, ignoreRootfs bool) error {
}
}

// Folder containing archived volumes that will be included in the export
expVolDir := filepath.Join(c.bundlePath(), "volumes")

// Create an archive for each volume associated with the container
if !options.IgnoreVolumes {
if err := os.MkdirAll(expVolDir, 0700); err != nil {
return errors.Wrapf(err, "error creating volumes export directory %q", expVolDir)
}

for _, v := range c.config.NamedVolumes {
volumeTarFilePath := filepath.Join("volumes", v.Name+".tar")
volumeTarFileFullPath := filepath.Join(c.bundlePath(), volumeTarFilePath)

volumeTarFile, err := os.Create(volumeTarFileFullPath)
if err != nil {
return errors.Wrapf(err, "error creating %q", volumeTarFileFullPath)
}

volume, err := c.runtime.GetVolume(v.Name)
if err != nil {
return err
}

input, err := archive.TarWithOptions(volume.MountPoint(), &archive.TarOptions{
Compression: archive.Uncompressed,
IncludeSourceDir: true,
})
if err != nil {
return errors.Wrapf(err, "error reading volume directory %q", v.Dest)
}

_, err = io.Copy(volumeTarFile, input)
if err != nil {
return err
}
volumeTarFile.Close()

includeFiles = append(includeFiles, volumeTarFilePath)
}
}

input, err := archive.TarWithOptions(c.bundlePath(), &archive.TarOptions{
Compression: archive.Gzip,
IncludeSourceDir: true,
Expand All @@ -880,13 +921,13 @@ func (c *Container) exportCheckpoint(dest string, ignoreRootfs bool) error {
return errors.Wrapf(err, "error reading checkpoint directory %q", c.ID())
}

outFile, err := os.Create(dest)
outFile, err := os.Create(options.TargetFile)
if err != nil {
return errors.Wrapf(err, "error creating checkpoint export file %q", dest)
return errors.Wrapf(err, "error creating checkpoint export file %q", options.TargetFile)
}
defer outFile.Close()

if err := os.Chmod(dest, 0600); err != nil {
if err := os.Chmod(options.TargetFile, 0600); err != nil {
return err
}

Expand All @@ -898,6 +939,10 @@ func (c *Container) exportCheckpoint(dest string, ignoreRootfs bool) error {
os.Remove(rootfsDiffPath)
os.Remove(deleteFilesList)

if !options.IgnoreVolumes {
os.RemoveAll(expVolDir)
}

return nil
}

Expand Down Expand Up @@ -963,7 +1008,7 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
defer c.newContainerEvent(events.Checkpoint)

if options.TargetFile != "" {
if err = c.exportCheckpoint(options.TargetFile, options.IgnoreRootfs); err != nil {
if err = c.exportCheckpoint(options); err != nil {
return err
}
}
Expand Down Expand Up @@ -1193,6 +1238,30 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti
return err
}

// When restoring from an imported archive, allow restoring the content of volumes.
// Volumes are created in setupContainer()
if options.TargetFile != "" && !options.IgnoreVolumes {
for _, v := range c.config.NamedVolumes {
rst0git marked this conversation as resolved.
Show resolved Hide resolved
volumeFilePath := filepath.Join(c.bundlePath(), "volumes", v.Name+".tar")

volumeFile, err := os.Open(volumeFilePath)
if err != nil {
return errors.Wrapf(err, "Failed to open volume file %s", volumeFilePath)
}
defer volumeFile.Close()

volume, err := c.runtime.GetVolume(v.Name)
if err != nil {
return errors.Wrapf(err, "Failed to retrieve volume %s", v.Name)
}

mountPoint := volume.MountPoint()
if err := archive.UntarUncompressed(volumeFile, mountPoint, nil); err != nil {
return errors.Wrapf(err, "Failed to extract volume %s to %s", volumeFilePath, mountPoint)
}
}
}

// Before actually restarting the container, apply the root file-system changes
if !options.IgnoreRootfs {
rootfsDiffPath := filepath.Join(c.bundlePath(), "rootfs-diff.tar")
Expand Down
1 change: 1 addition & 0 deletions pkg/api/handlers/libpod/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ func Restore(w http.ResponseWriter, r *http.Request) {
Import bool `schema:"import"`
Name string `schema:"name"`
IgnoreRootFS bool `schema:"ignoreRootFS"`
IgnoreVolumes bool `schema:"ignoreVolumes"`
IgnoreStaticIP bool `schema:"ignoreStaticIP"`
IgnoreStaticMAC bool `schema:"ignoreStaticMAC"`
}{
Expand Down
29 changes: 22 additions & 7 deletions pkg/checkpoint/checkpoint_restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/image"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/errorhandling"
"github.com/containers/podman/v2/pkg/util"
"github.com/containers/storage/pkg/archive"
Expand Down Expand Up @@ -36,10 +37,10 @@ func crImportFromJSON(filePath string, v interface{}) error {

// 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, input string, name string) ([]*libpod.Container, error) {
func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions) ([]*libpod.Container, error) {
// First get the container definition from the
// tarball to a temporary directory
archiveFile, err := os.Open(input)
archiveFile, err := os.Open(restoreOptions.Import)
if err != nil {
return nil, errors.Wrap(err, "failed to open checkpoint archive for import")
}
Expand All @@ -53,6 +54,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri
"rootfs-diff.tar",
"network.status",
"deleted.files",
"volumes",
},
}
dir, err := ioutil.TempDir("", "checkpoint")
Expand All @@ -66,7 +68,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri
}()
err = archive.Untar(archiveFile, dir, options)
if err != nil {
return nil, errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", input)
return nil, errors.Wrapf(err, "Unpacking of checkpoint archive %s failed", restoreOptions.Import)
}

// Load spec.dump from temporary directory
Expand All @@ -82,17 +84,30 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input stri
}

// This should not happen as checkpoints with these options are not exported.
if (len(config.Dependencies) > 0) || (len(config.NamedVolumes) > 0) {
return nil, errors.Errorf("Cannot import checkpoints of containers with named volumes or dependencies")
if len(config.Dependencies) > 0 {
return nil, errors.Errorf("Cannot import checkpoints of containers with dependencies")
}

// Volumes included in the checkpoint should not exist
if !restoreOptions.IgnoreVolumes {
for _, vol := range config.NamedVolumes {
exists, err := runtime.HasVolume(vol.Name)
if err != nil {
return nil, err
}
if exists {
return nil, errors.Errorf("volume with name %s already exists. Use --ignore-volumes to not restore content of volumes", vol.Name)
}
}
}

ctrID := config.ID
newName := false

// Check if the restored container gets a new name
if name != "" {
if restoreOptions.Name != "" {
config.ID = ""
config.Name = name
config.Name = restoreOptions.Name
newName = true
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/domain/entities/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ type CheckpointOptions struct {
All bool
Export string
IgnoreRootFS bool
IgnoreVolumes bool
Keep bool
Latest bool
LeaveRunning bool
Expand All @@ -187,6 +188,7 @@ type CheckpointReport struct {
type RestoreOptions struct {
All bool
IgnoreRootFS bool
IgnoreVolumes bool
IgnoreStaticIP bool
IgnoreStaticMAC bool
Import string
Expand Down
4 changes: 3 additions & 1 deletion pkg/domain/infra/abi/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds [
TCPEstablished: options.TCPEstablished,
TargetFile: options.Export,
IgnoreRootfs: options.IgnoreRootFS,
IgnoreVolumes: options.IgnoreVolumes,
KeepRunning: options.LeaveRunning,
}

Expand Down Expand Up @@ -525,6 +526,7 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st
TargetFile: options.Import,
Name: options.Name,
IgnoreRootfs: options.IgnoreRootFS,
IgnoreVolumes: options.IgnoreVolumes,
IgnoreStaticIP: options.IgnoreStaticIP,
IgnoreStaticMAC: options.IgnoreStaticMAC,
}
Expand All @@ -538,7 +540,7 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st

switch {
case options.Import != "":
cons, err = checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options.Import, options.Name)
cons, err = checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options)
case options.All:
cons, err = ic.Libpod.GetContainers(filterFuncs...)
default:
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/build/basicalpine/Containerfile.volume
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM alpine
VOLUME "/volume0"
Loading