diff --git a/cmd/nerdctl/checkpoint/checkpoint.go b/cmd/nerdctl/checkpoint/checkpoint.go index 10a8c00108f..a0eb4fa125a 100644 --- a/cmd/nerdctl/checkpoint/checkpoint.go +++ b/cmd/nerdctl/checkpoint/checkpoint.go @@ -34,7 +34,15 @@ func Command() *cobra.Command { cmd.AddCommand( CreateCommand(), + checkpointLsCommand(), ) return cmd } + +func checkpointLsCommand() *cobra.Command { + x := ListCommand() + x.Use = "ls" + x.Aliases = []string{"list"} + return x +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_list.go b/cmd/nerdctl/checkpoint/checkpoint_list.go new file mode 100644 index 00000000000..75fa986fe33 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_list.go @@ -0,0 +1,95 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpoint + +import ( + "fmt" + "text/tabwriter" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" +) + +func ListCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "list [OPTIONS] CONTAINER", + Short: "List checkpoints for a container", + Args: cobra.ExactArgs(1), + RunE: listAction, + ValidArgsFunction: listShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") + return cmd +} + +func processListFlags(cmd *cobra.Command) (types.CheckpointListOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.CheckpointListOptions{}, err + } + + checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") + if err != nil { + return types.CheckpointListOptions{}, err + } + if checkpointDir == "" { + checkpointDir = globalOptions.DataRoot + "/checkpoints" + } + + return types.CheckpointListOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + CheckpointDir: checkpointDir, + }, nil +} + +func listAction(cmd *cobra.Command, args []string) error { + listOptions, err := processListFlags(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), listOptions.GOptions.Namespace, listOptions.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + checkpoints, err := checkpoint.List(ctx, client, args[0], listOptions) + if err != nil { + return err + } + + w := tabwriter.NewWriter(listOptions.Stdout, 4, 8, 4, ' ', 0) + fmt.Fprintln(w, "CHECKPOINT NAME") + + for _, cp := range checkpoints { + fmt.Fprintln(w, cp.Name) + } + + return w.Flush() +} + +func listShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go new file mode 100644 index 00000000000..bb1aec502c4 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go @@ -0,0 +1,107 @@ +//go:build linux + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpoint + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestCheckpointListErrors(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) + + testCase.SubTests = []*test.Case{ + { + Description: "too-few-arguments", + Command: test.Command("checkpoint", "list"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ExitCode: 1} + }, + }, + { + Description: "too-many-arguments", + Command: test.Command("checkpoint", "list", "too", "many", "arguments"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ExitCode: 1} + }, + }, + { + Description: "invalid-container-id", + Command: test.Command("checkpoint", "list", "no-such-container"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("error list checkpoint for container: no-such-container")}, + } + }, + }, + } + + testCase.Run(t) +} + +func TestCheckpointList(t *testing.T) { + const checkpointName = "checkpoint-list" + + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("checkpoint", "create", data.Identifier(), checkpointName) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "list", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + // First line is header, second should include the checkpoint name + Output: expect.Contains("CHECKPOINT NAME\n" + checkpointName + "\n"), + } + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 7fba7931258..aef4ea5080a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -55,6 +55,7 @@ - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) - [Checkpoint management](#checkpoint-management) - [:whale: nerdctl checkpoint create](#whale-nerdctl-checkpoint-create) + - [:whale: nerdctl checkpoint list](#whale-nerdctl-checkpoint-list) - [Manifest management](#manifest-management) - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) @@ -1076,6 +1077,15 @@ Flags: - :whale: `--leave-running`: Leave the container running after checkpoint - :whale: `checkpoint-dir`: Use a custom checkpoint storage directory +### :whale: nerdctl checkpoint list + +List checkpoints for a container + +Usage: `nerdctl checkpoint list/ls [OPTIONS] CONTAINER` + +Flags: +- :whale: `checkpoint-dir`: Use a custom checkpoint storage directory + ## Manifest management ### :whale: nerdctl manifest annotate diff --git a/pkg/api/types/checkpoint_types.go b/pkg/api/types/checkpoint_types.go index 46b055105c4..61b5c3ead47 100644 --- a/pkg/api/types/checkpoint_types.go +++ b/pkg/api/types/checkpoint_types.go @@ -27,3 +27,15 @@ type CheckpointCreateOptions struct { // Checkpoint directory CheckpointDir string } + +type CheckpointListOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Checkpoint directory + CheckpointDir string +} + +type CheckpointSummary struct { + // Name is the name of the checkpoint. + Name string +} diff --git a/pkg/cmd/checkpoint/list.go b/pkg/cmd/checkpoint/list.go new file mode 100644 index 00000000000..b8007d0f5ab --- /dev/null +++ b/pkg/cmd/checkpoint/list.go @@ -0,0 +1,71 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpoint + +import ( + "context" + "fmt" + "os" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/checkpointutil" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" +) + +func List(ctx context.Context, client *containerd.Client, containerID string, options types.CheckpointListOptions) ([]types.CheckpointSummary, error) { + var container containerd.Container + var out []types.CheckpointSummary + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple containers found with provided prefix: %s", found.Req) + } + container = found.Container + return nil + }, + } + + n, err := walker.Walk(ctx, containerID) + if err != nil { + return nil, err + } else if n == 0 { + return nil, fmt.Errorf("error list checkpoint for container: %s, no such container", containerID) + } + + checkpointDir, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, "", container.ID(), false) + if err != nil { + return nil, err + } + + dirs, err := os.ReadDir(checkpointDir) + if err != nil { + return nil, err + } + + for _, d := range dirs { + if !d.IsDir() { + continue + } + out = append(out, types.CheckpointSummary{Name: d.Name()}) + } + + return out, nil +}