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 to convert checkpoint archives to OCI images #125

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ jobs:
- uses: actions/checkout@v3
- name: Install tools
run: |
sudo dnf -y install ShellCheck bats golang criu asciidoctor iptables iproute kmod jq bash bash-completion zsh fish
sudo dnf -y install ShellCheck shfmt bats golang criu asciidoctor iptables iproute kmod jq bash bash-completion zsh fish
sudo modprobe -va ip_tables ip6table_filter nf_conntrack nf_conntrack_netlink
- name: Run make shfmt-lint
run: make shfmt-lint
- name: Run make shellcheck
run: make shellcheck
- name: Run make all
Expand Down
21 changes: 17 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
SHELL = /bin/bash
PREFIX ?= $(DESTDIR)/usr/local
BINDIR ?= $(PREFIX)/bin
SCRIPTDIR ?= $(DESTDIR)/usr/libexec
GO ?= go
GOPATH := $(shell $(GO) env GOPATH)
GOBIN := $(shell $(GO) env GOBIN)
GO_SRC = $(shell find . -name \*.go)
GO_BUILD = $(GO) build
NAME = checkpointctl
SCRIPTNAME = build_image.sh

BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions
ZSHINSTALLDIR=${PREFIX}/share/zsh/site-functions
Expand Down Expand Up @@ -52,17 +54,22 @@ 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)$(SCRIPTDIR)

.PHONY: uninstall
uninstall: uninstall.completions
@make -C docs uninstall
@echo " UNINSTALL" $(NAME)
@$(RM) $(addprefix $(DESTDIR)$(BINDIR)/,$(NAME))
@$(RM) $(addprefix $(DESTDIR)$(BINDIR)/,$(NAME)) $(addprefix $(DESTDIR)$(SCRIPTDIR)/,$(SCRIPTNAME))

.PHONY: clean
clean:
Expand All @@ -77,9 +84,14 @@ golang-lint:
.PHONY: shellcheck
shellcheck:
shellcheck test/*bats
shellcheck internal/scripts/build_image.sh

.PHONY: shfmt-lint
shfmt-lint:
shfmt -w -d internal/scripts/build_image.sh

.PHONY: lint
lint: golang-lint shellcheck
lint: golang-lint shellcheck shfmt-lint

.PHONY: test
test: $(NAME)
Expand Down Expand Up @@ -151,9 +163,10 @@ help:
@echo " * completions - generate auto-completion files"
@echo " * clean - remove artifacts"
@echo " * docs - build man pages"
@echo " * lint - verify the source code (shellcheck/golangci-lint)"
@echo " * lint - verify the source code (shellcheck/golangci-lint/shfmt-lint)"
@echo " * golang-lint - run golangci-lint"
@echo " * shellcheck - run shellcheck"
@echo " * shfmt-lint - run shfmt on selected shell scripts"
@echo " * vendor - update go.mod, go.sum, and vendor directory"
@echo " * test - run tests"
@echo " * test-junit - run tests and create junit output"
Expand Down
2 changes: 2 additions & 0 deletions checkpointctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func main() {

rootCommand.AddCommand(cmd.List())

rootCommand.AddCommand(cmd.BuildCmd())

rootCommand.Version = version

if err := rootCommand.Execute(); err != nil {
Expand Down
41 changes: 41 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 25 additions & 0 deletions docs/checkpointctl-build.adoc
Original file line number Diff line number Diff line change
@@ -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)
113 changes: 113 additions & 0 deletions internal/image_from_checkpoint_archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package internal

import (
"bytes"
"context"
"fmt"
"log"
"os"
"os/exec"

metadata "github.com/checkpoint-restore/checkpointctl/lib"
)

const (
BUILD_SCRIPT = "/usr/libexec/build_image.sh"
PODMAN_ENGINE = "libpod"
)

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, "-a", annotationsFilePath, "-c", ic.checkpointPath, "-i", 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{}
checkpointImageAnnotations[metadata.CheckpointAnnotationContainerManager] = info.containerInfo.Engine
checkpointImageAnnotations[metadata.CheckpointAnnotationName] = info.containerInfo.Name
checkpointImageAnnotations[metadata.CheckpointAnnotationPod] = info.containerInfo.Pod
checkpointImageAnnotations[metadata.CheckpointAnnotationNamespace] = info.containerInfo.Namespace
checkpointImageAnnotations[metadata.CheckpointAnnotationRootfsImage] = info.configDump.RootfsImage
checkpointImageAnnotations[metadata.CheckpointAnnotationRootfsImageName] = info.configDump.RootfsImageName
checkpointImageAnnotations[metadata.CheckpointAnnotationRootfsImageID] = info.configDump.RootfsImageRef
checkpointImageAnnotations[metadata.CheckpointAnnotationRuntimeName] = info.configDump.OCIRuntime

annotationsFilePath, err := writeAnnotationsToFile(tempDir, checkpointImageAnnotations)
if err != nil {
return "", err
}

return annotationsFilePath, nil
}
78 changes: 78 additions & 0 deletions internal/scripts/build_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/bash

set -euo pipefail

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bash has a builtin optarg parser which would be nicer than purely positional parameters.

Also add checks that all the input files actually exist.

usage() {
cat <<EOF
Usage: ${0##*/} [-a ANNOTATIONS_FILE] [-c CHECKPOINT_PATH] [-i IMAGE_NAME]
Create OCI image from a checkpoint tar file.

-a path to the annotations file
-c path to the checkpoint file
-i name of the resulting image
EOF
exit 1
}

annotationsFilePath=""
checkpointPath=""
imageName=""

while getopts ":a:c:i:" opt; do
case ${opt} in
a)
annotationsFilePath=$OPTARG
;;
c)
checkpointPath=$OPTARG
;;
i)
imageName=$OPTARG
;;
:)
echo "Option -$OPTARG requires an argument."
usage
;;
\?)
echo "Invalid option: -$OPTARG"
usage
;;
esac
done
shift $((OPTIND - 1))

if [[ -z $annotationsFilePath || -z $checkpointPath || -z $imageName ]]; then
echo "All options (-a, -c, -i) are required."
usage
fi

if ! command -v buildah &>/dev/null; then
echo "buildah is not installed. Please install buildah before running 'checkpointctl build' command."
exit 1
fi

if [[ ! -f $annotationsFilePath ]]; then
echo "Annotations file not found: $annotationsFilePath"
exit 1
fi

if [[ ! -f $checkpointPath ]]; then
echo "Checkpoint file not found: $checkpointPath"
exit 1
fi

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"
35 changes: 35 additions & 0 deletions lib/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package metadata

const (
// CheckpointAnnotationContainerManager is used when creating an OCI image
// from a checkpoint archive to specify the name of container manager.
CheckpointAnnotationContainerManager = "checkpointctl.annotation.container.manager"

// CheckpointAnnotationName is used when creating an OCI image
// from a checkpoint archive to specify the name of the checkpoint.
CheckpointAnnotationName = "checkpointctl.annotation.checkpoint.name"

// CheckpointAnnotationPod is used when creating an OCI image
// from a checkpoint archive to specify the name of the pod associated with the checkpoint.
CheckpointAnnotationPod = "checkpointctl.annotation.checkpoint.pod"

// CheckpointAnnotationNamespace is used when creating an OCI image
// from a checkpoint archive to specify the namespace of the pod associated with the checkpoint.
CheckpointAnnotationNamespace = "checkpointctl.annotation.checkpoint.namespace"

// CheckpointAnnotationRootfsImage is used when creating an OCI image
// from a checkpoint archive to specify the root filesystem image associated with the checkpoint.
CheckpointAnnotationRootfsImage = "checkpointctl.annotation.checkpoint.rootfsImage"

// CheckpointAnnotationRootfsImageID is used when creating an OCI image
// from a checkpoint archive to specify the ID of the root filesystem image associated with the checkpoint.
CheckpointAnnotationRootfsImageID = "checkpointctl.annotation.checkpoint.rootfsImageID"

// CheckpointAnnotationRootfsImageName is used when creating an OCI image
// from a checkpoint archive to specify the name of the root filesystem image associated with the checkpoint.
CheckpointAnnotationRootfsImageName = "checkpointctl.annotation.checkpoint.rootfsImageName"

// CheckpointAnnotationRuntimeName is used when creating an OCI image
// from a checkpoint archive to specify the runtime used on the host where the checkpoint was created.
CheckpointAnnotationRuntimeName = "checkpointctl.annotation.checkpoint.runtime.name"
)
Loading