From 8496613fafbbfbc77efa7fee9aab08f8c3f37033 Mon Sep 17 00:00:00 2001 From: Parthiba-Hazra Date: Wed, 3 Apr 2024 18:36:24 +0530 Subject: [PATCH] Add support to build OCI images from checkpoint archives - With this enhancement, users can now build OCI images from checkpoint archives using the `checkpointctl build` command. This command accepts checkpoint archive and a image name as input and generates an OCI image suitable for use with container runtimes like CRI-O or Podman. Users can inspect the image to get information about runtime, container, pod, namespace, image name etc. Signed-off-by: Parthiba-Hazra --- Makefile | 7 +- checkpointctl.go | 2 + cmd/build.go | 41 +++++++ docs/checkpointctl-build.adoc | 25 +++++ internal/annotations.go | 83 ++++++++++++++ internal/image_from_checkpoint_archive.go | 129 ++++++++++++++++++++++ internal/scripts/build_image.sh | 28 +++++ 7 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 cmd/build.go create mode 100644 docs/checkpointctl-build.adoc create mode 100644 internal/annotations.go create mode 100644 internal/image_from_checkpoint_archive.go create mode 100644 internal/scripts/build_image.sh diff --git a/Makefile b/Makefile index 24d93da3..70167af7 100644 --- a/Makefile +++ b/Makefile @@ -52,12 +52,17 @@ release: CGO_ENABLED=0 $(GO_BUILD) -o $(NAME) -ldflags "-X main.name=$(NAME) -X main.version=${VERSION}" .PHONY: install -install: $(NAME) install.completions +install: $(NAME) install.completions install-scripts @echo " INSTALL " $< @mkdir -p $(DESTDIR)$(BINDIR) @install -m0755 $< $(DESTDIR)$(BINDIR) @make -C docs install +.PHONY: install-scripts +install-scripts: + @echo " INSTALL SCRIPTS" + @install -m0755 internal/scripts/build_image.sh $(DESTDIR)$(BINDIR) + .PHONY: uninstall uninstall: uninstall.completions @make -C docs uninstall diff --git a/checkpointctl.go b/checkpointctl.go index 14848af3..3851ba20 100644 --- a/checkpointctl.go +++ b/checkpointctl.go @@ -31,6 +31,8 @@ func main() { rootCommand.AddCommand(cmd.List()) + rootCommand.AddCommand(cmd.BuildCmd()) + rootCommand.Version = version if err := rootCommand.Execute(); err != nil { diff --git a/cmd/build.go b/cmd/build.go new file mode 100644 index 00000000..e0f63146 --- /dev/null +++ b/cmd/build.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "context" + "fmt" + "log" + + "github.com/checkpoint-restore/checkpointctl/internal" + "github.com/spf13/cobra" +) + +func BuildCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "build [checkpoint-path] [image-name]", + Short: "Create an OCI image from a container checkpoint archive", + RunE: convertArchive, + } + + return cmd +} + +func convertArchive(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return fmt.Errorf("please provide both the checkpoint path and the image name") + } + + checkpointPath := args[0] + imageName := args[1] + + imageCreater := internal.NewImageCreator(imageName, checkpointPath) + + err := imageCreater.CreateImageFromCheckpoint(context.Background()) + if err != nil { + return err + } + + log.Printf("Image '%s' created successfully from checkpoint '%s'\n", imageName, checkpointPath) + return nil +} diff --git a/docs/checkpointctl-build.adoc b/docs/checkpointctl-build.adoc new file mode 100644 index 00000000..8fec3817 --- /dev/null +++ b/docs/checkpointctl-build.adoc @@ -0,0 +1,25 @@ += checkpointctl-build(1) +include::footer.adoc[] + +== Name + +*checkpointctl-build* - Create OCI image from a checkpoint tar file. + +== Synopsis + +*checkpointctl build* CHECKPOINT_PATH IMAGE_NAME + +== Options + +*-h*, *--help*:: + Show help for checkpointctl build + +== Description + +Creates an OCI image from a checkpoint tar file. This command requires `buildah` to be installed on the system. + +Please ensure that `buildah` is installed before running this command. + +== See also + +checkpointctl(1) diff --git a/internal/annotations.go b/internal/annotations.go new file mode 100644 index 00000000..335de8c9 --- /dev/null +++ b/internal/annotations.go @@ -0,0 +1,83 @@ +package internal + +const ( + // CheckpointAnnotationContianerMnager is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of container manager. + CheckpointAnnotationContianerMnager = "io.container.manager" + + // CrioCheckpointAnnotationName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the checkpoint. + CrioCheckpointAnnotationName = "io.kubernetes.cri-o.annotations.checkpoint.name" + + // CrioCheckpointAnnotationPod is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the pod associated with the checkpoint. + CrioCheckpointAnnotationPod = "io.kubernetes.cri-o.annotations.checkpoint.pod" + + // CrioCheckpointAnnotationNamespace is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the namespace of the pod associated with the checkpoint. + CrioCheckpointAnnotationNamespace = "io.kubernetes.cri-o.annotations.checkpoint.namespace" + + // CrioCheckpointAnnotationRootfsImage is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the root filesystem image associated with the checkpoint. + CrioCheckpointAnnotationRootfsImage = "io.kubernetes.cri-o.annotations.checkpoint.rootfsImage" + + // CrioCheckpointAnnotationRootfsImageID is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the ID of the root filesystem image associated with the checkpoint. + CrioCheckpointAnnotationRootfsImageID = "io.kubernetes.cri-o.annotations.checkpoint.rootfsImageID" + + // CrioCheckpointAnnotationRootfsImageName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the root filesystem image associated with the checkpoint. + CrioCheckpointAnnotationRootfsImageName = "io.kubernetes.cri-o.annotations.checkpoint.rootfsImageName" + + // CrioCheckpointAnnotationRuntimeName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the runtime used on the host where the checkpoint was created. + CrioCheckpointAnnotationRuntimeName = "io.kubernetes.cri-o.annotations.checkpoint.runtime.name" + + // ContainerdCheckpointAnnotationName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the checkpoint. + ContainerdCheckpointAnnotationName = "io.kubernetes.cri.annotations.checkpoint.name" + + // ContainerdCheckpointAnnotationPod is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the pod associated with the checkpoint. + ContainerdCheckpointAnnotationPod = "io.kubernetes.cri.annotations.checkpoint.pod" + + // ContainerdCheckpointAnnotationNamespace is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the namespace of the pod associated with the checkpoint. + ContainerdCheckpointAnnotationNamespace = "io.kubernetes.cri.annotations.checkpoint.namespace" + + // ContainerdCheckpointAnnotationRootfsImage is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the root filesystem image associated with the checkpoint. + ContainerdCheckpointAnnotationRootfsImage = "io.kubernetes.cri.annotations.checkpoint.rootfsImage" + + // ContainerdCheckpointAnnotationRootfsImageID is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the ID of the root filesystem image associated with the checkpoint. + ContainerdCheckpointAnnotationRootfsImageID = "io.kubernetes.cri.annotations.checkpoint.rootfsImageID" + + // ContainerdCheckpointAnnotationRootfsImageName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the root filesystem image associated with the checkpoint. + ContainerdCheckpointAnnotationRootfsImageName = "io.kubernetes.cri.annotations.checkpoint.rootfsImageName" + + // ContainerdCheckpointAnnotationRuntimeName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the runtime used on the host where the checkpoint was created. + ContainerdCheckpointAnnotationRuntimeName = "io.kubernetes.cri.annotations.checkpoint.runtime.name" + + // PodmanCheckpointAnnotationName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the checkpoint. + PodmanCheckpointAnnotationName = "io.podman.annotations.checkpoint.name" + + // PodmanCheckpointAnnotationRootfsImage is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the root filesystem image associated with the checkpoint. + PodmanCheckpointAnnotationRootfsImage = "io.podman.annotations.checkpoint.rootfsImage" + + // PodmanCheckpointAnnotationRootfsImageID is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the ID of the root filesystem image associated with the checkpoint. + PodmanCheckpointAnnotationRootfsImageID = "io.podman.annotations.checkpoint.rootfsImageID" + + // PodmanCheckpointAnnotationRootfsImageName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the name of the root filesystem image associated with the checkpoint. + PodmanCheckpointAnnotationRootfsImageName = "io.podman.annotations.checkpoint.rootfsImageName" + + // PodmanCheckpointAnnotationRuntimeName is used by checkpointctl when creating an OCI image + // from a checkpoint archive to specify the runtime used on the host where the checkpoint was created. + PodmanCheckpointAnnotationRuntimeName = "io.podman.annotations.checkpoint.runtime.name" +) diff --git a/internal/image_from_checkpoint_archive.go b/internal/image_from_checkpoint_archive.go new file mode 100644 index 00000000..03723c9f --- /dev/null +++ b/internal/image_from_checkpoint_archive.go @@ -0,0 +1,129 @@ +package internal + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + "os/exec" + + metadata "github.com/checkpoint-restore/checkpointctl/lib" +) + +const BUILD_SCRIPT = "build_image.sh" + +type ImageCreator struct { + imageName string + checkpointPath string +} + +func NewImageCreator(imageName, checkpointPath string) *ImageCreator { + return &ImageCreator{ + imageName: imageName, + checkpointPath: checkpointPath, + } +} + +func (ic *ImageCreator) CreateImageFromCheckpoint(ctx context.Context) error { + tempDir, err := os.MkdirTemp("", "checkpoint_tmp") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + annotationsFilePath, err := ic.setCheckpointAnnotations(tempDir) + if err != nil { + return err + } + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd := exec.Command(BUILD_SCRIPT, annotationsFilePath, ic.checkpointPath, ic.imageName) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() + if err != nil { + return fmt.Errorf("failed to execute script: %v, %v, %w", stdout.String(), stderr.String(), err) + } + + return nil +} + +func writeAnnotationsToFile(tempDir string, annotations map[string]string) (string, error) { + tempFile, err := os.CreateTemp(tempDir, "annotations_*.txt") + if err != nil { + return "", err + } + defer tempFile.Close() + + for key, value := range annotations { + _, err := fmt.Fprintf(tempFile, "%s=%s\n", key, value) + if err != nil { + return "", err + } + } + + return tempFile.Name(), nil +} + +func (ic *ImageCreator) setCheckpointAnnotations(tempDir string) (string, error) { + filesToExtract := []string{"spec.dump", "config.dump"} + if err := UntarFiles(ic.checkpointPath, tempDir, filesToExtract); err != nil { + log.Printf("Error extracting files from archive %s: %v\n", ic.checkpointPath, err) + return "", err + } + + var err error + info := &checkpointInfo{} + info.configDump, _, err = metadata.ReadContainerCheckpointConfigDump(tempDir) + if err != nil { + return "", err + } + + info.specDump, _, err = metadata.ReadContainerCheckpointSpecDump(tempDir) + if err != nil { + return "", err + } + + info.containerInfo, err = getContainerInfo(info.specDump, info.configDump) + if err != nil { + return "", err + } + + checkpointImageAnnotations := map[string]string{} + switch info.containerInfo.Engine { + case "CRI-O": + checkpointImageAnnotations[CheckpointAnnotationContianerMnager] = info.containerInfo.Engine + checkpointImageAnnotations[CrioCheckpointAnnotationName] = info.containerInfo.Name + checkpointImageAnnotations[CrioCheckpointAnnotationPod] = info.containerInfo.Pod + checkpointImageAnnotations[CrioCheckpointAnnotationNamespace] = info.containerInfo.Namespace + checkpointImageAnnotations[CrioCheckpointAnnotationRootfsImage] = info.configDump.RootfsImage + checkpointImageAnnotations[CrioCheckpointAnnotationRootfsImageName] = info.configDump.RootfsImageName + checkpointImageAnnotations[CrioCheckpointAnnotationRootfsImageID] = info.configDump.RootfsImageRef + checkpointImageAnnotations[CrioCheckpointAnnotationRuntimeName] = info.configDump.OCIRuntime + case "libpod": + checkpointImageAnnotations[CheckpointAnnotationContianerMnager] = info.containerInfo.Engine + checkpointImageAnnotations[PodmanCheckpointAnnotationName] = info.containerInfo.Name + checkpointImageAnnotations[PodmanCheckpointAnnotationRootfsImage] = info.configDump.RootfsImage + checkpointImageAnnotations[PodmanCheckpointAnnotationRootfsImageName] = info.configDump.RootfsImageName + checkpointImageAnnotations[PodmanCheckpointAnnotationRootfsImageID] = info.configDump.RootfsImageRef + checkpointImageAnnotations[PodmanCheckpointAnnotationRuntimeName] = info.configDump.OCIRuntime + default: + checkpointImageAnnotations[CheckpointAnnotationContianerMnager] = info.containerInfo.Engine + checkpointImageAnnotations[ContainerdCheckpointAnnotationName] = info.containerInfo.Name + checkpointImageAnnotations[ContainerdCheckpointAnnotationPod] = info.containerInfo.Pod + checkpointImageAnnotations[ContainerdCheckpointAnnotationNamespace] = info.containerInfo.Namespace + checkpointImageAnnotations[ContainerdCheckpointAnnotationRootfsImage] = info.configDump.RootfsImage + checkpointImageAnnotations[ContainerdCheckpointAnnotationRootfsImageName] = info.configDump.RootfsImageName + checkpointImageAnnotations[ContainerdCheckpointAnnotationRootfsImageID] = info.configDump.RootfsImageRef + checkpointImageAnnotations[ContainerdCheckpointAnnotationRuntimeName] = info.configDump.OCIRuntime + } + + annotationsFilePath, err := writeAnnotationsToFile(tempDir, checkpointImageAnnotations) + if err != nil { + return "", err + } + + return annotationsFilePath, nil +} diff --git a/internal/scripts/build_image.sh b/internal/scripts/build_image.sh new file mode 100644 index 00000000..56dd3c4e --- /dev/null +++ b/internal/scripts/build_image.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -euo pipefail + +if ! command -v buildah &> /dev/null; then + echo "buildah is not installed. Please install buildah before running 'checkpointctl build' command." + exit 1 +fi + +annotationsFilePath="$1" +checkpointPath="$2" +imageName="$3" + +newcontainer=$(buildah from scratch) + +buildah add "$newcontainer" "$checkpointPath" + +while IFS= read -r line; do + key=$(echo "$line" | cut -d '=' -f 1) + value=$(echo "$line" | cut -d '=' -f 2-) + buildah config --annotation "$key=$value" "$newcontainer" +done < "$annotationsFilePath" + +buildah commit "$newcontainer" "$imageName" + +buildah rm "$newcontainer" + +echo "Checkpoint image created successfully: $imageName"