From 3a362462c1b6a4f52bd8e4f47e1a4f16f2441584 Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Mon, 21 Nov 2022 10:04:24 +0000 Subject: [PATCH 1/3] test: Add tests for checkpoint images These tests were unintentionally removed in commit b47b48f (Revert "Add checkpoint image tests"). They verify the functionality of the `--create-image` option for `podman container checkpoint`. Signed-off-by: Radostin Stoyanov --- test/e2e/checkpoint_image_test.go | 296 ++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 test/e2e/checkpoint_image_test.go diff --git a/test/e2e/checkpoint_image_test.go b/test/e2e/checkpoint_image_test.go new file mode 100644 index 000000000000..6f8ef1664345 --- /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() { + SkipIfContainerized("FIXME: #15015. All checkpoint tests hang when containerized.") + SkipIfRootless("checkpoint not supported in rootless mode") + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + // 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).To(HaveField("CheckpointPath", "")) + Expect(inspectOut[0].State).To(HaveField("CheckpointLog", "")) + Expect(inspectOut[0].State).To(HaveField("RestoreLog", "")) + + 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 checkpoint 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 checkpoint 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)) + }) +}) From f4401567cdf6d7ccc1ef9f50345c865bc6262c22 Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Sun, 20 Nov 2022 20:48:38 +0000 Subject: [PATCH 2/3] Enable 'podman run' for checkpoint images This patch extends the podman run command with support for checkpoint images. When `podman run` is invoked with an image that contains a checkpoint, it would restore the container from that checkpoint. Example: podman run -d --name looper busybox /bin/sh -c \ 'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done' podman container checkpoint --create-image checkpoint-image-1 looper podman run checkpoint-image-1 Signed-off-by: Radostin Stoyanov --- pkg/domain/infra/abi/containers.go | 39 +++++++++++++++++++++++++++ pkg/domain/infra/tunnel/containers.go | 25 +++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index d34d69b1ee77..e4aead7fa3c9 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "reflect" "strconv" "sync" "time" @@ -1110,6 +1111,44 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta fmt.Fprintf(os.Stderr, "%s\n", w) } + if opts.Spec != nil && !reflect.ValueOf(opts.Spec).IsNil() { + // If this is a checkpoint image, restore it. + img, resolvedImageName := opts.Spec.GetImage() + if img != nil && resolvedImageName != "" { + imgData, err := img.Inspect(ctx, nil) + if err != nil { + return nil, err + } + if imgData != nil { + _, isCheckpointImage := imgData.Annotations[define.CheckpointAnnotationRuntimeName] + if isCheckpointImage { + var restoreOptions entities.RestoreOptions + restoreOptions.Name = opts.Spec.Name + restoreOptions.Pod = opts.Spec.Pod + responses, err := ic.ContainerRestore(ctx, []string{resolvedImageName}, restoreOptions) + if err != nil { + return nil, err + } + + report := entities.ContainerRunReport{} + for _, r := range responses { + report.Id = r.Id + report.ExitCode = 0 + if r.Err != nil { + logrus.Errorf("Failed to restore checkpoint image %s: %v", resolvedImageName, r.Err) + report.ExitCode = 126 + } + if r.RawInput != "" { + logrus.Errorf("Failed to restore checkpoint image %s: %v", resolvedImageName, r.RawInput) + report.ExitCode = 126 + } + } + return &report, nil + } + } + } + } + rtSpec, spec, optsN, err := generate.MakeContainer(ctx, ic.Libpod, opts.Spec, false, nil) if err != nil { return nil, err diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index f3e66982d8ec..e8a09a755979 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "reflect" "strconv" "strings" "sync" @@ -790,6 +791,30 @@ func (ic *ContainerEngine) ContainerListExternal(ctx context.Context) ([]entitie } func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.ContainerRunOptions) (*entities.ContainerRunReport, error) { + if opts.Spec != nil && !reflect.ValueOf(opts.Spec).IsNil() && opts.Spec.RawImageName != "" { + // If this is a checkpoint image, restore it. + getImageOptions := new(images.GetOptions).WithSize(false) + inspectReport, err := images.GetImage(ic.ClientCtx, opts.Spec.RawImageName, getImageOptions) + if err != nil { + return nil, fmt.Errorf("no such container or image: %s", opts.Spec.RawImageName) + } + if inspectReport != nil { + _, isCheckpointImage := inspectReport.Annotations[define.CheckpointAnnotationRuntimeName] + if isCheckpointImage { + restoreOptions := new(containers.RestoreOptions) + restoreOptions.WithName(opts.Spec.Name) + restoreOptions.WithPod(opts.Spec.Pod) + + restoreReport, err := containers.Restore(ic.ClientCtx, inspectReport.ID, restoreOptions) + if err != nil { + return nil, err + } + runReport := entities.ContainerRunReport{Id: restoreReport.Id} + return &runReport, nil + } + } + } + con, err := containers.CreateWithSpec(ic.ClientCtx, opts.Spec, nil) if err != nil { return nil, err From a93a390b8c29c797b11934ec1d1ce3463cffa8dd Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Mon, 21 Nov 2022 12:23:17 +0000 Subject: [PATCH 3/3] test: podman run with checkpoint image Signed-off-by: Radostin Stoyanov --- test/e2e/checkpoint_image_test.go | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/e2e/checkpoint_image_test.go b/test/e2e/checkpoint_image_test.go index 6f8ef1664345..6558d78c64dc 100644 --- a/test/e2e/checkpoint_image_test.go +++ b/test/e2e/checkpoint_image_test.go @@ -293,4 +293,51 @@ var _ = Describe("Podman checkpoint", func() { Expect(result).Should(Exit(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) }) + + It("podman run with checkpoint image", func() { + // Container image must be lowercase + checkpointImage := "alpine-checkpoint-" + strings.ToLower(RandomString(6)) + containerName := "alpine-container-" + RandomString(6) + + // Create container + localRunString := []string{"run", "-d", "--name", containerName, ALPINE, "top"} + session := podmanTest.Podman(localRunString) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + containerID1 := session.OutputToString() + + // Checkpoint container, create checkpoint image + result := podmanTest.Podman([]string{"container", "checkpoint", "--create-image", checkpointImage, "--keep", containerID1}) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) + + // Remove existing container + result = podmanTest.Podman([]string{"rm", "-t", "1", "-f", containerName}) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + + // Restore containers from image using `podman run` + result = podmanTest.Podman([]string{"run", checkpointImage}) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(podmanTest.NumberOfContainersRunning()).To(Equal(1)) + + // Check if the container is running + status := podmanTest.Podman([]string{"inspect", containerName, "--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)) + }) })