Skip to content

Commit c8ddcd8

Browse files
committed
checkpoint: support nerdctl checkpoint create command
- Create checkpoints from running containers using containerd APIs - Support both leave-running and exit modes via --leave-running flag - Configurable checkpoint directory via --checkpoint-dir flag Signed-off-by: ChengyuZhu6 <[email protected]>
1 parent 4d28f8b commit c8ddcd8

File tree

6 files changed

+349
-0
lines changed

6 files changed

+349
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package checkpoint
18+
19+
import (
20+
"github.com/spf13/cobra"
21+
22+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
23+
)
24+
25+
func Command() *cobra.Command {
26+
cmd := &cobra.Command{
27+
Annotations: map[string]string{helpers.Category: helpers.Management},
28+
Use: "checkpoint",
29+
Short: "Manage checkpoints.",
30+
RunE: helpers.UnknownSubcommandAction,
31+
SilenceUsage: true,
32+
SilenceErrors: true,
33+
}
34+
35+
cmd.AddCommand(
36+
CreateCommand(),
37+
)
38+
39+
return cmd
40+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package checkpoint
18+
19+
import (
20+
"path/filepath"
21+
22+
"github.com/spf13/cobra"
23+
24+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
25+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
26+
"github.com/containerd/nerdctl/v2/pkg/api/types"
27+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
28+
"github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint"
29+
)
30+
31+
func CreateCommand() *cobra.Command {
32+
var cmd = &cobra.Command{
33+
Use: "create [OPTIONS] CONTAINER CHECKPOINT",
34+
Short: "Create a checkpoint from a running container",
35+
Args: cobra.ExactArgs(2),
36+
RunE: createAction,
37+
ValidArgsFunction: createShellComplete,
38+
SilenceUsage: true,
39+
SilenceErrors: true,
40+
}
41+
cmd.Flags().Bool("leave-running", false, "Leave the container running after checkpointing")
42+
cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory")
43+
return cmd
44+
}
45+
46+
func processCreateFlags(cmd *cobra.Command) (types.CheckpointCreateOptions, error) {
47+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
48+
if err != nil {
49+
return types.CheckpointCreateOptions{}, err
50+
}
51+
52+
leaveRunning, err := cmd.Flags().GetBool("leave-running")
53+
if err != nil {
54+
return types.CheckpointCreateOptions{}, err
55+
}
56+
checkpointDir, err := cmd.Flags().GetString("checkpoint-dir")
57+
if err != nil {
58+
return types.CheckpointCreateOptions{}, err
59+
}
60+
if checkpointDir == "" {
61+
checkpointDir = filepath.Join(globalOptions.DataRoot, "checkpoints")
62+
}
63+
64+
return types.CheckpointCreateOptions{
65+
Stdout: cmd.OutOrStdout(),
66+
GOptions: globalOptions,
67+
LeaveRunning: leaveRunning,
68+
CheckpointDir: checkpointDir,
69+
}, nil
70+
}
71+
72+
func createAction(cmd *cobra.Command, args []string) error {
73+
createOptions, err := processCreateFlags(cmd)
74+
if err != nil {
75+
return err
76+
}
77+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), createOptions.GOptions.Namespace, createOptions.GOptions.Address)
78+
if err != nil {
79+
return err
80+
}
81+
defer cancel()
82+
83+
err = checkpoint.Create(ctx, client, args[0], args[1], createOptions)
84+
if err != nil {
85+
return err
86+
}
87+
88+
return nil
89+
}
90+
91+
func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
92+
return completion.ImageNames(cmd)
93+
}

cmd/nerdctl/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/containerd/log"
3232

3333
"github.com/containerd/nerdctl/v2/cmd/nerdctl/builder"
34+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/checkpoint"
3435
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
3536
"github.com/containerd/nerdctl/v2/cmd/nerdctl/compose"
3637
"github.com/containerd/nerdctl/v2/cmd/nerdctl/container"
@@ -350,6 +351,9 @@ Config file ($NERDCTL_TOML): %s
350351

351352
// Manifest
352353
manifest.Command(),
354+
355+
// Checkpoint
356+
checkpoint.Command(),
353357
)
354358
addApparmorCommand(rootCmd)
355359
container.AddCpCommand(rootCmd)

pkg/api/types/checkpoint_types.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package types
18+
19+
import "io"
20+
21+
// CheckpointCreateOptions specifies options for `nerdctl checkpoint create`.
22+
type CheckpointCreateOptions struct {
23+
Stdout io.Writer
24+
GOptions GlobalCommandOptions
25+
// Leave the container running after checkpointing
26+
LeaveRunning bool
27+
// Checkpoint directory
28+
CheckpointDir string
29+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package checkpointutil
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
)
24+
25+
func GetCheckpointDir(checkpointDir, checkpointID, containerID string, create bool) (string, error) {
26+
checkpointAbsDir := filepath.Join(checkpointDir, checkpointID)
27+
stat, err := os.Stat(checkpointAbsDir)
28+
if create {
29+
switch {
30+
case err == nil && stat.IsDir():
31+
err = fmt.Errorf("checkpoint with name %s already exists for container %s", checkpointID, containerID)
32+
case err != nil && os.IsNotExist(err):
33+
err = os.MkdirAll(checkpointAbsDir, 0o700)
34+
case err != nil:
35+
err = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir)
36+
}
37+
} else {
38+
switch {
39+
case err != nil:
40+
err = fmt.Errorf("checkpoint %s does not exist for container %s", checkpointID, containerID)
41+
case stat.IsDir():
42+
err = nil
43+
default:
44+
err = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir)
45+
}
46+
}
47+
return checkpointAbsDir, err
48+
}

pkg/cmd/checkpoint/create.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package checkpoint
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"errors"
23+
"fmt"
24+
25+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
26+
27+
"github.com/containerd/containerd/api/types/runc/options"
28+
containerd "github.com/containerd/containerd/v2/client"
29+
"github.com/containerd/containerd/v2/core/content"
30+
"github.com/containerd/containerd/v2/core/images"
31+
"github.com/containerd/containerd/v2/pkg/archive"
32+
"github.com/containerd/containerd/v2/plugins"
33+
34+
"github.com/containerd/nerdctl/v2/pkg/api/types"
35+
"github.com/containerd/nerdctl/v2/pkg/checkpointutil"
36+
"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
37+
)
38+
39+
func Create(ctx context.Context, client *containerd.Client, containerID string, checkpointName string, options types.CheckpointCreateOptions) error {
40+
var container containerd.Container
41+
42+
walker := &containerwalker.ContainerWalker{
43+
Client: client,
44+
OnFound: func(ctx context.Context, found containerwalker.Found) error {
45+
if found.MatchCount > 1 {
46+
return fmt.Errorf("multiple containers found with provided prefix: %s", found.Req)
47+
}
48+
container = found.Container
49+
return nil
50+
},
51+
}
52+
53+
n, err := walker.Walk(ctx, containerID)
54+
if err != nil {
55+
return err
56+
} else if n == 0 {
57+
return fmt.Errorf("error creating checkpoint for container: %s, no such container", containerID)
58+
}
59+
60+
info, err := container.Info(ctx)
61+
if err != nil {
62+
return fmt.Errorf("failed to get info for container %q: %w", containerID, err)
63+
}
64+
65+
task, err := container.Task(ctx, nil)
66+
if err != nil {
67+
return fmt.Errorf("failed to get task for container %q: %w", containerID, err)
68+
}
69+
70+
img, err := task.Checkpoint(ctx, withCheckpointOpts(info.Runtime.Name, !options.LeaveRunning))
71+
if err != nil {
72+
return err
73+
}
74+
75+
defer client.ImageService().Delete(ctx, img.Name())
76+
77+
cs := client.ContentStore()
78+
79+
rawIndex, err := content.ReadBlob(ctx, cs, img.Target())
80+
if err != nil {
81+
return fmt.Errorf("failed to retrieve checkpoint data: %w", err)
82+
}
83+
84+
var index ocispec.Index
85+
if err := json.Unmarshal(rawIndex, &index); err != nil {
86+
return fmt.Errorf("failed to decode checkpoint data: %w", err)
87+
}
88+
89+
var cpDesc *ocispec.Descriptor
90+
for _, m := range index.Manifests {
91+
if m.MediaType == images.MediaTypeContainerd1Checkpoint {
92+
cpDesc = &m //nolint:gosec
93+
break
94+
}
95+
}
96+
if cpDesc == nil {
97+
return errors.New("invalid checkpoint")
98+
}
99+
100+
targetPath, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, checkpointName, container.ID(), true)
101+
if err != nil {
102+
return err
103+
}
104+
105+
rat, err := cs.ReaderAt(ctx, *cpDesc)
106+
if err != nil {
107+
return fmt.Errorf("failed to get checkpoint reader: %w", err)
108+
}
109+
defer rat.Close()
110+
111+
_, err = archive.Apply(ctx, targetPath, content.NewReader(rat))
112+
if err != nil {
113+
return fmt.Errorf("failed to read checkpoint reader: %w", err)
114+
}
115+
116+
fmt.Fprintf(options.Stdout, "%s\n", checkpointName)
117+
118+
return nil
119+
}
120+
121+
func withCheckpointOpts(rt string, exit bool) containerd.CheckpointTaskOpts {
122+
return func(r *containerd.CheckpointTaskInfo) error {
123+
124+
switch rt {
125+
case plugins.RuntimeRuncV2:
126+
if r.Options == nil {
127+
r.Options = &options.CheckpointOptions{}
128+
}
129+
opts, _ := r.Options.(*options.CheckpointOptions)
130+
131+
opts.Exit = exit
132+
}
133+
return nil
134+
}
135+
}

0 commit comments

Comments
 (0)