Skip to content

Commit 4bbe454

Browse files
authored
Merge pull request containerd#4515 from ChengyuZhu6/checkpoint-ls
checkpoint: support checkpoint ls command
2 parents 3c53779 + 2233f3f commit 4bbe454

File tree

6 files changed

+303
-0
lines changed

6 files changed

+303
-0
lines changed

cmd/nerdctl/checkpoint/checkpoint.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ func Command() *cobra.Command {
3434

3535
cmd.AddCommand(
3636
CreateCommand(),
37+
checkpointLsCommand(),
3738
)
3839

3940
return cmd
4041
}
42+
43+
func checkpointLsCommand() *cobra.Command {
44+
x := ListCommand()
45+
x.Use = "ls"
46+
x.Aliases = []string{"list"}
47+
return x
48+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
"fmt"
21+
"text/tabwriter"
22+
23+
"github.com/spf13/cobra"
24+
25+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
26+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
27+
"github.com/containerd/nerdctl/v2/pkg/api/types"
28+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
29+
"github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint"
30+
)
31+
32+
func ListCommand() *cobra.Command {
33+
var cmd = &cobra.Command{
34+
Use: "list [OPTIONS] CONTAINER",
35+
Short: "List checkpoints for a container",
36+
Args: cobra.ExactArgs(1),
37+
RunE: listAction,
38+
ValidArgsFunction: listShellComplete,
39+
SilenceUsage: true,
40+
SilenceErrors: true,
41+
}
42+
cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory")
43+
return cmd
44+
}
45+
46+
func processListFlags(cmd *cobra.Command) (types.CheckpointListOptions, error) {
47+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
48+
if err != nil {
49+
return types.CheckpointListOptions{}, err
50+
}
51+
52+
checkpointDir, err := cmd.Flags().GetString("checkpoint-dir")
53+
if err != nil {
54+
return types.CheckpointListOptions{}, err
55+
}
56+
if checkpointDir == "" {
57+
checkpointDir = globalOptions.DataRoot + "/checkpoints"
58+
}
59+
60+
return types.CheckpointListOptions{
61+
Stdout: cmd.OutOrStdout(),
62+
GOptions: globalOptions,
63+
CheckpointDir: checkpointDir,
64+
}, nil
65+
}
66+
67+
func listAction(cmd *cobra.Command, args []string) error {
68+
listOptions, err := processListFlags(cmd)
69+
if err != nil {
70+
return err
71+
}
72+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), listOptions.GOptions.Namespace, listOptions.GOptions.Address)
73+
if err != nil {
74+
return err
75+
}
76+
defer cancel()
77+
78+
checkpoints, err := checkpoint.List(ctx, client, args[0], listOptions)
79+
if err != nil {
80+
return err
81+
}
82+
83+
w := tabwriter.NewWriter(listOptions.Stdout, 4, 8, 4, ' ', 0)
84+
fmt.Fprintln(w, "CHECKPOINT NAME")
85+
86+
for _, cp := range checkpoints {
87+
fmt.Fprintln(w, cp.Name)
88+
}
89+
90+
return w.Flush()
91+
}
92+
93+
func listShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
94+
return completion.ImageNames(cmd)
95+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//go:build linux
2+
3+
/*
4+
Copyright The containerd Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package checkpoint
20+
21+
import (
22+
"errors"
23+
"testing"
24+
25+
"github.com/containerd/nerdctl/mod/tigron/expect"
26+
"github.com/containerd/nerdctl/mod/tigron/require"
27+
"github.com/containerd/nerdctl/mod/tigron/test"
28+
29+
"github.com/containerd/nerdctl/v2/pkg/testutil"
30+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
31+
)
32+
33+
func TestCheckpointListErrors(t *testing.T) {
34+
testCase := nerdtest.Setup()
35+
36+
testCase.Require = require.All(
37+
require.Not(nerdtest.Rootless),
38+
// Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality.
39+
// The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750.
40+
require.Not(nerdtest.Docker),
41+
)
42+
43+
testCase.SubTests = []*test.Case{
44+
{
45+
Description: "too-few-arguments",
46+
Command: test.Command("checkpoint", "list"),
47+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
48+
return &test.Expected{ExitCode: 1}
49+
},
50+
},
51+
{
52+
Description: "too-many-arguments",
53+
Command: test.Command("checkpoint", "list", "too", "many", "arguments"),
54+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
55+
return &test.Expected{ExitCode: 1}
56+
},
57+
},
58+
{
59+
Description: "invalid-container-id",
60+
Command: test.Command("checkpoint", "list", "no-such-container"),
61+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
62+
return &test.Expected{
63+
ExitCode: 1,
64+
Errors: []error{errors.New("error list checkpoint for container: no-such-container")},
65+
}
66+
},
67+
},
68+
}
69+
70+
testCase.Run(t)
71+
}
72+
73+
func TestCheckpointList(t *testing.T) {
74+
const checkpointName = "checkpoint-list"
75+
76+
testCase := nerdtest.Setup()
77+
testCase.Require = require.All(
78+
require.Not(nerdtest.Rootless),
79+
// Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality.
80+
// The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750.
81+
require.Not(nerdtest.Docker),
82+
)
83+
84+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
85+
helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity")
86+
helpers.Ensure("checkpoint", "create", data.Identifier(), checkpointName)
87+
}
88+
89+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
90+
helpers.Anyhow("rm", "-f", data.Identifier())
91+
helpers.Anyhow("rmi", "-f", testutil.CommonImage)
92+
}
93+
94+
testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
95+
return helpers.Command("checkpoint", "list", data.Identifier())
96+
}
97+
98+
testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected {
99+
return &test.Expected{
100+
ExitCode: 0,
101+
// First line is header, second should include the checkpoint name
102+
Output: expect.Contains("CHECKPOINT NAME\n" + checkpointName + "\n"),
103+
}
104+
}
105+
106+
testCase.Run(t)
107+
}

docs/command-reference.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
- [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt)
5656
- [Checkpoint management](#checkpoint-management)
5757
- [:whale: nerdctl checkpoint create](#whale-nerdctl-checkpoint-create)
58+
- [:whale: nerdctl checkpoint list](#whale-nerdctl-checkpoint-list)
5859
- [Manifest management](#manifest-management)
5960
- [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate)
6061
- [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create)
@@ -1076,6 +1077,15 @@ Flags:
10761077
- :whale: `--leave-running`: Leave the container running after checkpoint
10771078
- :whale: `checkpoint-dir`: Use a custom checkpoint storage directory
10781079

1080+
### :whale: nerdctl checkpoint list
1081+
1082+
List checkpoints for a container
1083+
1084+
Usage: `nerdctl checkpoint list/ls [OPTIONS] CONTAINER`
1085+
1086+
Flags:
1087+
- :whale: `checkpoint-dir`: Use a custom checkpoint storage directory
1088+
10791089
## Manifest management
10801090

10811091
### :whale: nerdctl manifest annotate

pkg/api/types/checkpoint_types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,15 @@ type CheckpointCreateOptions struct {
2727
// Checkpoint directory
2828
CheckpointDir string
2929
}
30+
31+
type CheckpointListOptions struct {
32+
Stdout io.Writer
33+
GOptions GlobalCommandOptions
34+
// Checkpoint directory
35+
CheckpointDir string
36+
}
37+
38+
type CheckpointSummary struct {
39+
// Name is the name of the checkpoint.
40+
Name string
41+
}

pkg/cmd/checkpoint/list.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
"fmt"
22+
"os"
23+
24+
containerd "github.com/containerd/containerd/v2/client"
25+
26+
"github.com/containerd/nerdctl/v2/pkg/api/types"
27+
"github.com/containerd/nerdctl/v2/pkg/checkpointutil"
28+
"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
29+
)
30+
31+
func List(ctx context.Context, client *containerd.Client, containerID string, options types.CheckpointListOptions) ([]types.CheckpointSummary, error) {
32+
var container containerd.Container
33+
var out []types.CheckpointSummary
34+
35+
walker := &containerwalker.ContainerWalker{
36+
Client: client,
37+
OnFound: func(ctx context.Context, found containerwalker.Found) error {
38+
if found.MatchCount > 1 {
39+
return fmt.Errorf("multiple containers found with provided prefix: %s", found.Req)
40+
}
41+
container = found.Container
42+
return nil
43+
},
44+
}
45+
46+
n, err := walker.Walk(ctx, containerID)
47+
if err != nil {
48+
return nil, err
49+
} else if n == 0 {
50+
return nil, fmt.Errorf("error list checkpoint for container: %s, no such container", containerID)
51+
}
52+
53+
checkpointDir, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, "", container.ID(), false)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
dirs, err := os.ReadDir(checkpointDir)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
for _, d := range dirs {
64+
if !d.IsDir() {
65+
continue
66+
}
67+
out = append(out, types.CheckpointSummary{Name: d.Name()})
68+
}
69+
70+
return out, nil
71+
}

0 commit comments

Comments
 (0)