Skip to content

Commit 1cf0f64

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 ae7c0e5 commit 1cf0f64

File tree

7 files changed

+304
-1
lines changed

7 files changed

+304
-1
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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/containerd/nerdctl/v2/cmd/nerdctl/helpers"
21+
"github.com/spf13/cobra"
22+
)
23+
24+
func Command() *cobra.Command {
25+
cmd := &cobra.Command{
26+
Annotations: map[string]string{helpers.Category: helpers.Management},
27+
Use: "checkpoint",
28+
Short: "Manage checkpoints.",
29+
RunE: helpers.UnknownSubcommandAction,
30+
SilenceUsage: true,
31+
SilenceErrors: true,
32+
}
33+
34+
cmd.AddCommand(
35+
CreateCommand(),
36+
)
37+
38+
return cmd
39+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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/containerd/nerdctl/v2/cmd/nerdctl/completion"
21+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
22+
"github.com/containerd/nerdctl/v2/pkg/api/types"
23+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
24+
"github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint"
25+
"github.com/spf13/cobra"
26+
)
27+
28+
func CreateCommand() *cobra.Command {
29+
var cmd = &cobra.Command{
30+
Use: "create [OPTIONS] CONTAINER CHECKPOINT",
31+
Short: "Create a checkpoint from a running container",
32+
Args: cobra.ExactArgs(2),
33+
RunE: createAction,
34+
ValidArgsFunction: createShellComplete,
35+
SilenceUsage: true,
36+
SilenceErrors: true,
37+
}
38+
cmd.Flags().Bool("leave-running", false, "Leave the container running after checkpointing")
39+
cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory")
40+
return cmd
41+
}
42+
43+
func processCreateFlags(cmd *cobra.Command) (types.CheckpointCreateOptions, error) {
44+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
45+
if err != nil {
46+
return types.CheckpointCreateOptions{}, err
47+
}
48+
49+
leaveRunning, err := cmd.Flags().GetBool("leave-running")
50+
if err != nil {
51+
return types.CheckpointCreateOptions{}, err
52+
}
53+
checkpointDir, err := cmd.Flags().GetString("checkpoint-dir")
54+
if err != nil {
55+
return types.CheckpointCreateOptions{}, err
56+
}
57+
if checkpointDir == "" {
58+
checkpointDir = globalOptions.DataRoot + "/checkpoints"
59+
}
60+
61+
return types.CheckpointCreateOptions{
62+
Stdout: cmd.OutOrStdout(),
63+
GOptions: globalOptions,
64+
LeaveRunning: leaveRunning,
65+
CheckpointDir: checkpointDir,
66+
}, nil
67+
}
68+
69+
func createAction(cmd *cobra.Command, args []string) error {
70+
createOptions, err := processCreateFlags(cmd)
71+
if err != nil {
72+
return err
73+
}
74+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), createOptions.GOptions.Namespace, createOptions.GOptions.Address)
75+
if err != nil {
76+
return err
77+
}
78+
defer cancel()
79+
80+
err = checkpoint.Create(ctx, client, args[0], args[1], createOptions)
81+
if err != nil {
82+
return err
83+
}
84+
85+
return nil
86+
}
87+
88+
func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
89+
return completion.ImageNames(cmd)
90+
}

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)

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ require (
116116
github.com/opencontainers/selinux v1.12.0 // indirect
117117
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
118118
github.com/philhofer/fwd v1.2.0 // indirect
119-
github.com/pkg/errors v0.9.1 // indirect
119+
github.com/pkg/errors v0.9.1
120120
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
121121
github.com/sasha-s/go-deadlock v0.3.5 // indirect
122122
//gomodjail:unconfined
@@ -148,4 +148,6 @@ require (
148148
tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect
149149
)
150150

151+
require github.com/containerd/containerd v1.7.23
152+
151153
replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJ
2727
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
2828
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
2929
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
30+
github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ=
31+
github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
3032
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0=
3133
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
3234
github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ=

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+
}

pkg/cmd/checkpoint/create.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
"fmt"
23+
"os"
24+
"path/filepath"
25+
26+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
27+
28+
"github.com/containerd/containerd/api/types/runc/options"
29+
"github.com/containerd/containerd/archive"
30+
"github.com/containerd/containerd/v2/client"
31+
containerd "github.com/containerd/containerd/v2/client"
32+
"github.com/containerd/containerd/v2/core/content"
33+
"github.com/containerd/containerd/v2/core/images"
34+
"github.com/containerd/containerd/v2/plugins"
35+
36+
"github.com/containerd/nerdctl/v2/pkg/api/types"
37+
"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
38+
"github.com/pkg/errors"
39+
)
40+
41+
func Create(ctx context.Context, client *containerd.Client, containerID string, checkpointName string, options types.CheckpointCreateOptions) error {
42+
var container containerd.Container
43+
44+
walker := &containerwalker.ContainerWalker{
45+
Client: client,
46+
OnFound: func(ctx context.Context, found containerwalker.Found) error {
47+
if found.MatchCount > 1 {
48+
return fmt.Errorf("multiple containers found with provided prefix: %s", found.Req)
49+
}
50+
container = found.Container
51+
return nil
52+
},
53+
}
54+
55+
n, err := walker.Walk(ctx, containerID)
56+
if err != nil {
57+
return err
58+
} else if n == 0 {
59+
return fmt.Errorf("error creating checkpoint for container: %s, no such container", containerID)
60+
}
61+
62+
info, err := container.Info(ctx)
63+
if err != nil {
64+
return fmt.Errorf("failed to get info for container %q: %w", containerID, err)
65+
}
66+
67+
task, err := container.Task(ctx, nil)
68+
if err != nil {
69+
return fmt.Errorf("failed to get task for container %q: %w", containerID, err)
70+
}
71+
72+
img, err := task.Checkpoint(ctx, withCheckpointOpts(info.Runtime.Name, options.LeaveRunning))
73+
if err != nil {
74+
return err
75+
}
76+
77+
defer client.ImageService().Delete(ctx, img.Name())
78+
79+
cs := client.ContentStore()
80+
81+
rawIndex, err := content.ReadBlob(ctx, cs, img.Target())
82+
if err != nil {
83+
return fmt.Errorf("failed to retrieve checkpoint data: %w", err)
84+
}
85+
86+
var index ocispec.Index
87+
if err := json.Unmarshal(rawIndex, &index); err != nil {
88+
return fmt.Errorf("failed to decode checkpoint data: %w", err)
89+
}
90+
91+
var cpDesc *ocispec.Descriptor
92+
for _, m := range index.Manifests {
93+
if m.MediaType == images.MediaTypeContainerd1Checkpoint {
94+
cpDesc = &m //nolint:gosec
95+
break
96+
}
97+
}
98+
if cpDesc == nil {
99+
return errors.New("invalid checkpoint")
100+
}
101+
102+
targetPath := filepath.Join(options.CheckpointDir, checkpointName)
103+
if err := os.MkdirAll(targetPath, 0o700); err != nil {
104+
return err
105+
}
106+
107+
rat, err := cs.ReaderAt(ctx, *cpDesc)
108+
if err != nil {
109+
return fmt.Errorf("failed to get checkpoint reader: %w", err)
110+
}
111+
defer rat.Close()
112+
113+
_, err = archive.Apply(ctx, targetPath, content.NewReader(rat))
114+
if err != nil {
115+
return fmt.Errorf("failed to read checkpoint reader: %w", err)
116+
}
117+
118+
fmt.Fprintf(options.Stdout, "%s\n", checkpointName)
119+
120+
return nil
121+
}
122+
123+
func withCheckpointOpts(rt string, exit bool) client.CheckpointTaskOpts {
124+
return func(r *client.CheckpointTaskInfo) error {
125+
126+
switch rt {
127+
case plugins.RuntimeRuncV2:
128+
if r.Options == nil {
129+
r.Options = &options.CheckpointOptions{}
130+
}
131+
opts, _ := r.Options.(*options.CheckpointOptions)
132+
133+
opts.Exit = exit
134+
}
135+
return nil
136+
}
137+
}

0 commit comments

Comments
 (0)