diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index d34d69b1ee..e4aead7fa3 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 f3e66982d8..e8a09a7559 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 diff --git a/test/e2e/checkpoint_image_test.go b/test/e2e/checkpoint_image_test.go new file mode 100644 index 0000000000..6558d78c64 --- /dev/null +++ b/test/e2e/checkpoint_image_test.go @@ -0,0 +1,343 @@ +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)) + }) + + 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)) + }) +})