Skip to content

Commit 24b08c2

Browse files
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 <parthibahazra@gmail.com>
1 parent 9a448d5 commit 24b08c2

File tree

8 files changed

+303
-59
lines changed

8 files changed

+303
-59
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ jobs:
1212
- uses: actions/checkout@v4
1313
- name: Install tools
1414
run: |
15-
sudo dnf -y install ShellCheck bats golang criu asciidoctor iptables iproute kmod jq bash bash-completion zsh fish
15+
sudo dnf -y install ShellCheck shfmt bats golang criu asciidoctor iptables iproute kmod jq bash bash-completion zsh fish
1616
sudo modprobe -va ip_tables ip6table_filter nf_conntrack nf_conntrack_netlink
17+
- name: Run make shfmt-lint
18+
run: make shfmt-lint
1719
- name: Run make shellcheck
1820
run: make shellcheck
1921
- name: Run make all

Makefile

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
SHELL = /bin/bash
22
PREFIX ?= $(DESTDIR)/usr/local
33
BINDIR ?= $(PREFIX)/bin
4+
SCRIPTDIR ?= $(DESTDIR)/usr/libexec
45
GO ?= go
56
GOPATH := $(shell $(GO) env GOPATH)
67
GOBIN := $(shell $(GO) env GOBIN)
78
GO_SRC = $(shell find . -name \*.go)
89
GO_BUILD = $(GO) build
910
NAME = checkpointctl
11+
SCRIPTNAME = build_image.sh
1012

1113
BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions
1214
ZSHINSTALLDIR=${PREFIX}/share/zsh/site-functions
@@ -33,17 +35,22 @@ release:
3335
CGO_ENABLED=0 $(GO_BUILD) -o $(NAME) -ldflags "-X main.name=$(NAME) -X main.version=${VERSION}"
3436

3537
.PHONY: install
36-
install: $(NAME) install.completions
38+
install: $(NAME) install.completions install-scripts
3739
@echo " INSTALL " $<
3840
@mkdir -p $(DESTDIR)$(BINDIR)
3941
@install -m0755 $< $(DESTDIR)$(BINDIR)
4042
@make -C docs install
4143

44+
.PHONY: install-scripts
45+
install-scripts:
46+
@echo " INSTALL SCRIPTS"
47+
@install -m0755 internal/scripts/build_image.sh $(DESTDIR)$(SCRIPTDIR)
48+
4249
.PHONY: uninstall
4350
uninstall: uninstall.completions
4451
@make -C docs uninstall
4552
@echo " UNINSTALL" $(NAME)
46-
@$(RM) $(addprefix $(DESTDIR)$(BINDIR)/,$(NAME))
53+
@$(RM) $(addprefix $(DESTDIR)$(BINDIR)/,$(NAME)) $(addprefix $(DESTDIR)$(SCRIPTDIR)/,$(SCRIPTNAME))
4754

4855
.PHONY: clean
4956
clean:
@@ -58,9 +65,14 @@ golang-lint:
5865
.PHONY: shellcheck
5966
shellcheck:
6067
shellcheck test/*bats
68+
shellcheck internal/scripts/build_image.sh
69+
70+
.PHONY: shfmt-lint
71+
shfmt-lint:
72+
shfmt -w -d internal/scripts/build_image.sh
6173

6274
.PHONY: lint
63-
lint: golang-lint shellcheck
75+
lint: golang-lint shellcheck shfmt-lint
6476

6577
.PHONY: test
6678
test: $(NAME)
@@ -126,9 +138,10 @@ help:
126138
@echo " * completions - generate auto-completion files"
127139
@echo " * clean - remove artifacts"
128140
@echo " * docs - build man pages"
129-
@echo " * lint - verify the source code (shellcheck/golangci-lint)"
141+
@echo " * lint - verify the source code (shellcheck/golangci-lint/shfmt-lint)"
130142
@echo " * golang-lint - run golangci-lint"
131143
@echo " * shellcheck - run shellcheck"
144+
@echo " * shfmt-lint - run shfmt on selected shell scripts"
132145
@echo " * vendor - update go.mod, go.sum, and vendor directory"
133146
@echo " * test - run tests"
134147
@echo " * test-junit - run tests and create junit output"

checkpointctl.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func main() {
3131

3232
rootCommand.AddCommand(cmd.List())
3333

34+
rootCommand.AddCommand(cmd.BuildCmd())
35+
3436
rootCommand.Version = version
3537

3638
if err := rootCommand.Execute(); err != nil {

cmd/build.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package cmd
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"log"
9+
10+
"github.com/checkpoint-restore/checkpointctl/internal"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func BuildCmd() *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "build [checkpoint-path] [image-name]",
17+
Short: "Create an OCI image from a container checkpoint archive",
18+
RunE: convertArchive,
19+
}
20+
21+
return cmd
22+
}
23+
24+
func convertArchive(cmd *cobra.Command, args []string) error {
25+
if len(args) != 2 {
26+
return fmt.Errorf("please provide both the checkpoint path and the image name")
27+
}
28+
29+
checkpointPath := args[0]
30+
imageName := args[1]
31+
32+
imageCreater := internal.NewImageCreator(imageName, checkpointPath)
33+
34+
err := imageCreater.CreateImageFromCheckpoint(context.Background())
35+
if err != nil {
36+
return err
37+
}
38+
39+
log.Printf("Image '%s' created successfully from checkpoint '%s'\n", imageName, checkpointPath)
40+
return nil
41+
}

docs/checkpointctl-build.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
= checkpointctl-build(1)
2+
include::footer.adoc[]
3+
4+
== Name
5+
6+
*checkpointctl-build* - Create OCI image from a checkpoint tar file.
7+
8+
== Synopsis
9+
10+
*checkpointctl build* CHECKPOINT_PATH IMAGE_NAME
11+
12+
== Options
13+
14+
*-h*, *--help*::
15+
Show help for checkpointctl build
16+
17+
== Description
18+
19+
Creates an OCI image from a checkpoint tar file. This command requires `buildah` to be installed on the system.
20+
21+
Please ensure that `buildah` is installed before running this command.
22+
23+
== See also
24+
25+
checkpointctl(1)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package internal
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"log"
8+
"os"
9+
"os/exec"
10+
11+
metadata "github.com/checkpoint-restore/checkpointctl/lib"
12+
)
13+
14+
const (
15+
BUILD_SCRIPT = "/usr/libexec/build_image.sh"
16+
PODMAN_ENGINE = "libpod"
17+
)
18+
19+
type ImageCreator struct {
20+
imageName string
21+
checkpointPath string
22+
}
23+
24+
func NewImageCreator(imageName, checkpointPath string) *ImageCreator {
25+
return &ImageCreator{
26+
imageName: imageName,
27+
checkpointPath: checkpointPath,
28+
}
29+
}
30+
31+
func (ic *ImageCreator) CreateImageFromCheckpoint(ctx context.Context) error {
32+
tempDir, err := os.MkdirTemp("", "checkpoint_tmp")
33+
if err != nil {
34+
return err
35+
}
36+
defer os.RemoveAll(tempDir)
37+
38+
annotationsFilePath, err := ic.setCheckpointAnnotations(tempDir)
39+
if err != nil {
40+
return err
41+
}
42+
43+
var stdout bytes.Buffer
44+
var stderr bytes.Buffer
45+
cmd := exec.Command(BUILD_SCRIPT, "-a", annotationsFilePath, "-c", ic.checkpointPath, "-i", ic.imageName)
46+
cmd.Stdout = &stdout
47+
cmd.Stderr = &stderr
48+
err = cmd.Run()
49+
if err != nil {
50+
return fmt.Errorf("failed to execute script: %v, %v, %w", stdout.String(), stderr.String(), err)
51+
}
52+
53+
return nil
54+
}
55+
56+
func writeAnnotationsToFile(tempDir string, annotations map[string]string) (string, error) {
57+
tempFile, err := os.CreateTemp(tempDir, "annotations_*.txt")
58+
if err != nil {
59+
return "", err
60+
}
61+
defer tempFile.Close()
62+
63+
for key, value := range annotations {
64+
_, err := fmt.Fprintf(tempFile, "%s=%s\n", key, value)
65+
if err != nil {
66+
return "", err
67+
}
68+
}
69+
70+
return tempFile.Name(), nil
71+
}
72+
73+
func (ic *ImageCreator) setCheckpointAnnotations(tempDir string) (string, error) {
74+
filesToExtract := []string{"spec.dump", "config.dump"}
75+
if err := UntarFiles(ic.checkpointPath, tempDir, filesToExtract); err != nil {
76+
log.Printf("Error extracting files from archive %s: %v\n", ic.checkpointPath, err)
77+
return "", err
78+
}
79+
80+
var err error
81+
info := &checkpointInfo{}
82+
info.configDump, _, err = metadata.ReadContainerCheckpointConfigDump(tempDir)
83+
if err != nil {
84+
return "", err
85+
}
86+
87+
info.specDump, _, err = metadata.ReadContainerCheckpointSpecDump(tempDir)
88+
if err != nil {
89+
return "", err
90+
}
91+
92+
info.containerInfo, err = getContainerInfo(info.specDump, info.configDump)
93+
if err != nil {
94+
return "", err
95+
}
96+
97+
checkpointImageAnnotations := map[string]string{}
98+
checkpointImageAnnotations[metadata.CheckpointAnnotationContainerManager] = info.containerInfo.Engine
99+
checkpointImageAnnotations[metadata.CheckpointAnnotationName] = info.containerInfo.Name
100+
checkpointImageAnnotations[metadata.CheckpointAnnotationPod] = info.containerInfo.Pod
101+
checkpointImageAnnotations[metadata.CheckpointAnnotationNamespace] = info.containerInfo.Namespace
102+
checkpointImageAnnotations[metadata.CheckpointAnnotationRootfsImage] = info.configDump.RootfsImage
103+
checkpointImageAnnotations[metadata.CheckpointAnnotationRootfsImageName] = info.configDump.RootfsImageName
104+
checkpointImageAnnotations[metadata.CheckpointAnnotationRootfsImageID] = info.configDump.RootfsImageRef
105+
checkpointImageAnnotations[metadata.CheckpointAnnotationRuntimeName] = info.configDump.OCIRuntime
106+
107+
annotationsFilePath, err := writeAnnotationsToFile(tempDir, checkpointImageAnnotations)
108+
if err != nil {
109+
return "", err
110+
}
111+
112+
return annotationsFilePath, nil
113+
}

internal/scripts/build_image.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
usage() {
6+
cat <<EOF
7+
Usage: ${0##*/} [-a ANNOTATIONS_FILE] [-c CHECKPOINT_PATH] [-i IMAGE_NAME]
8+
Create OCI image from a checkpoint tar file.
9+
10+
-a path to the annotations file
11+
-c path to the checkpoint file
12+
-i name of the resulting image
13+
EOF
14+
exit 1
15+
}
16+
17+
annotationsFilePath=""
18+
checkpointPath=""
19+
imageName=""
20+
21+
while getopts ":a:c:i:" opt; do
22+
case ${opt} in
23+
a)
24+
annotationsFilePath=$OPTARG
25+
;;
26+
c)
27+
checkpointPath=$OPTARG
28+
;;
29+
i)
30+
imageName=$OPTARG
31+
;;
32+
:)
33+
echo "Option -$OPTARG requires an argument."
34+
usage
35+
;;
36+
\?)
37+
echo "Invalid option: -$OPTARG"
38+
usage
39+
;;
40+
esac
41+
done
42+
shift $((OPTIND - 1))
43+
44+
if [[ -z $annotationsFilePath || -z $checkpointPath || -z $imageName ]]; then
45+
echo "All options (-a, -c, -i) are required."
46+
usage
47+
fi
48+
49+
if ! command -v buildah &>/dev/null; then
50+
echo "buildah is not installed. Please install buildah before running 'checkpointctl build' command."
51+
exit 1
52+
fi
53+
54+
if [[ ! -f $annotationsFilePath ]]; then
55+
echo "Annotations file not found: $annotationsFilePath"
56+
exit 1
57+
fi
58+
59+
if [[ ! -f $checkpointPath ]]; then
60+
echo "Checkpoint file not found: $checkpointPath"
61+
exit 1
62+
fi
63+
64+
newcontainer=$(buildah from scratch)
65+
66+
buildah add "$newcontainer" "$checkpointPath"
67+
68+
while IFS= read -r line; do
69+
key=$(echo "$line" | cut -d '=' -f 1)
70+
value=$(echo "$line" | cut -d '=' -f 2-)
71+
buildah config --annotation "$key=$value" "$newcontainer"
72+
done <"$annotationsFilePath"
73+
74+
buildah commit "$newcontainer" "$imageName"
75+
76+
buildah rm "$newcontainer"
77+
78+
echo "Checkpoint image created successfully: $imageName"

0 commit comments

Comments
 (0)