From 0826362a4b8f3f15cb1cdf1ef8dda73a006b7884 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 26 Aug 2025 14:51:09 +0800 Subject: [PATCH 1/4] 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 --- cmd/nerdctl/checkpoint/checkpoint.go | 40 ++++++ cmd/nerdctl/checkpoint/checkpoint_create.go | 91 +++++++++++++ cmd/nerdctl/main.go | 4 + go.mod | 2 + go.sum | 2 + pkg/api/types/checkpoint_types.go | 29 +++++ pkg/checkpointutil/checkpointutil.go | 48 +++++++ pkg/cmd/checkpoint/create.go | 135 ++++++++++++++++++++ 8 files changed, 351 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint.go create mode 100644 cmd/nerdctl/checkpoint/checkpoint_create.go create mode 100644 pkg/api/types/checkpoint_types.go create mode 100644 pkg/checkpointutil/checkpointutil.go create mode 100644 pkg/cmd/checkpoint/create.go diff --git a/cmd/nerdctl/checkpoint/checkpoint.go b/cmd/nerdctl/checkpoint/checkpoint.go new file mode 100644 index 00000000000..10a8c00108f --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint.go @@ -0,0 +1,40 @@ +/* + 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 ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Annotations: map[string]string{helpers.Category: helpers.Management}, + Use: "checkpoint", + Short: "Manage checkpoints.", + RunE: helpers.UnknownSubcommandAction, + SilenceUsage: true, + SilenceErrors: true, + } + + cmd.AddCommand( + CreateCommand(), + ) + + return cmd +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_create.go b/cmd/nerdctl/checkpoint/checkpoint_create.go new file mode 100644 index 00000000000..bf801c0f9d8 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_create.go @@ -0,0 +1,91 @@ +/* + 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 ( + "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 CreateCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "create [OPTIONS] CONTAINER CHECKPOINT", + Short: "Create a checkpoint from a running container", + Args: cobra.ExactArgs(2), + RunE: createAction, + ValidArgsFunction: createShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().Bool("leave-running", false, "Leave the container running after checkpointing") + cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") + return cmd +} + +func processCreateFlags(cmd *cobra.Command) (types.CheckpointCreateOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.CheckpointCreateOptions{}, err + } + + leaveRunning, err := cmd.Flags().GetBool("leave-running") + if err != nil { + return types.CheckpointCreateOptions{}, err + } + checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") + if err != nil { + return types.CheckpointCreateOptions{}, err + } + if checkpointDir == "" { + checkpointDir = globalOptions.DataRoot + "/checkpoints" + } + + return types.CheckpointCreateOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + LeaveRunning: leaveRunning, + CheckpointDir: checkpointDir, + }, nil +} + +func createAction(cmd *cobra.Command, args []string) error { + createOptions, err := processCreateFlags(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), createOptions.GOptions.Namespace, createOptions.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + err = checkpoint.Create(ctx, client, args[0], args[1], createOptions) + if err != nil { + return err + } + + return nil +} + +func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index c5abcc60a6c..51dfb26736e 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/checkpoint" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/compose" "github.com/containerd/nerdctl/v2/cmd/nerdctl/container" @@ -350,6 +351,9 @@ Config file ($NERDCTL_TOML): %s // Manifest manifest.Command(), + + // Checkpoint + checkpoint.Command(), ) addApparmorCommand(rootCmd) container.AddCpCommand(rootCmd) diff --git a/go.mod b/go.mod index 8d10651f6d0..9c878cff58b 100644 --- a/go.mod +++ b/go.mod @@ -148,4 +148,6 @@ require ( tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) +require github.com/containerd/containerd v1.7.23 + replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index 239f1ebb5e5..ff1c8bf0801 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJ github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ= +github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ= diff --git a/pkg/api/types/checkpoint_types.go b/pkg/api/types/checkpoint_types.go new file mode 100644 index 00000000000..46b055105c4 --- /dev/null +++ b/pkg/api/types/checkpoint_types.go @@ -0,0 +1,29 @@ +/* + 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 types + +import "io" + +// CheckpointCreateOptions specifies options for `nerdctl checkpoint create`. +type CheckpointCreateOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Leave the container running after checkpointing + LeaveRunning bool + // Checkpoint directory + CheckpointDir string +} diff --git a/pkg/checkpointutil/checkpointutil.go b/pkg/checkpointutil/checkpointutil.go new file mode 100644 index 00000000000..c3f789af737 --- /dev/null +++ b/pkg/checkpointutil/checkpointutil.go @@ -0,0 +1,48 @@ +/* + 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 checkpointutil + +import ( + "fmt" + "os" + "path/filepath" +) + +func GetCheckpointDir(checkpointDir, checkpointID, containerID string, create bool) (string, error) { + checkpointAbsDir := filepath.Join(checkpointDir, checkpointID) + stat, err := os.Stat(checkpointAbsDir) + if create { + switch { + case err == nil && stat.IsDir(): + err = fmt.Errorf("checkpoint with name %s already exists for container %s", checkpointID, containerID) + case err != nil && os.IsNotExist(err): + err = os.MkdirAll(checkpointAbsDir, 0o700) + case err != nil: + err = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) + } + } else { + switch { + case err != nil: + err = fmt.Errorf("checkpoint %s does not exist for container %s", checkpointID, containerID) + case stat.IsDir(): + err = nil + default: + err = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) + } + } + return checkpointAbsDir, err +} diff --git a/pkg/cmd/checkpoint/create.go b/pkg/cmd/checkpoint/create.go new file mode 100644 index 00000000000..297120226ed --- /dev/null +++ b/pkg/cmd/checkpoint/create.go @@ -0,0 +1,135 @@ +/* + 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" + "encoding/json" + "errors" + "fmt" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/api/types/runc/options" + "github.com/containerd/containerd/archive" + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/plugins" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/checkpointutil" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" +) + +func Create(ctx context.Context, client *containerd.Client, containerID string, checkpointName string, options types.CheckpointCreateOptions) error { + var container containerd.Container + + 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 err + } else if n == 0 { + return fmt.Errorf("error creating checkpoint for container: %s, no such container", containerID) + } + + info, err := container.Info(ctx) + if err != nil { + return fmt.Errorf("failed to get info for container %q: %w", containerID, err) + } + + task, err := container.Task(ctx, nil) + if err != nil { + return fmt.Errorf("failed to get task for container %q: %w", containerID, err) + } + + img, err := task.Checkpoint(ctx, withCheckpointOpts(info.Runtime.Name, !options.LeaveRunning)) + if err != nil { + return err + } + + defer client.ImageService().Delete(ctx, img.Name()) + + cs := client.ContentStore() + + rawIndex, err := content.ReadBlob(ctx, cs, img.Target()) + if err != nil { + return fmt.Errorf("failed to retrieve checkpoint data: %w", err) + } + + var index ocispec.Index + if err := json.Unmarshal(rawIndex, &index); err != nil { + return fmt.Errorf("failed to decode checkpoint data: %w", err) + } + + var cpDesc *ocispec.Descriptor + for _, m := range index.Manifests { + if m.MediaType == images.MediaTypeContainerd1Checkpoint { + cpDesc = &m //nolint:gosec + break + } + } + if cpDesc == nil { + return errors.New("invalid checkpoint") + } + + targetPath, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, checkpointName, container.ID(), true) + if err != nil { + return err + } + + rat, err := cs.ReaderAt(ctx, *cpDesc) + if err != nil { + return fmt.Errorf("failed to get checkpoint reader: %w", err) + } + defer rat.Close() + + _, err = archive.Apply(ctx, targetPath, content.NewReader(rat)) + if err != nil { + return fmt.Errorf("failed to read checkpoint reader: %w", err) + } + + fmt.Fprintf(options.Stdout, "%s\n", checkpointName) + + return nil +} + +func withCheckpointOpts(rt string, exit bool) containerd.CheckpointTaskOpts { + return func(r *containerd.CheckpointTaskInfo) error { + + switch rt { + case plugins.RuntimeRuncV2: + if r.Options == nil { + r.Options = &options.CheckpointOptions{} + } + opts, _ := r.Options.(*options.CheckpointOptions) + + opts.Exit = exit + } + return nil + } +} From 9df5f8acf29b35da9fdb8efdfd1655bc763f6115 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sat, 13 Sep 2025 20:08:01 +0800 Subject: [PATCH 2/4] checkpoint: add unit tests for checkpoint create command add unit tests for checkpoint create command. Signed-off-by: ChengyuZhu6 --- .../checkpoint_create_linux_test.go | 119 ++++++++++++++++++ cmd/nerdctl/checkpoint/checkpoint_test.go | 27 ++++ 2 files changed, 146 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go create mode 100644 cmd/nerdctl/checkpoint/checkpoint_test.go diff --git a/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go new file mode 100644 index 00000000000..4aef1bc58d8 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go @@ -0,0 +1,119 @@ +/* + 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/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestCheckpointCreateErrors(t *testing.T) { + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ + { + Description: "too-few-arguments", + Command: test.Command("checkpoint", "create", "too-few-arguments"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "too-many-arguments", + Command: test.Command("checkpoint", "create", "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", "create", "foo", "bar"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("error creating checkpoint for container: foo")}, + } + }, + }, + } + + testCase.Run(t) +} + +func TestCheckpointCreate(t *testing.T) { + const ( + checkpointName = "checkpoint-bar" + checkpointDir = "/dir/foo" + ) + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "leave-running=true", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("container-running"), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Custom("rm", "-rf", checkpointDir).Run(&test.Expected{ + ExitCode: expect.ExitCodeSuccess, + }) + helpers.Anyhow("rm", "-f", data.Identifier("container-running")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "create", "--leave-running", "--checkpoint-dir", checkpointDir, data.Identifier("container-running"), checkpointName) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals(checkpointName + "\n"), + } + }, + }, + { + Description: "leave-running=false", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("container-exit"), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Custom("rm", "-rf", checkpointDir).Run(&test.Expected{ + ExitCode: expect.ExitCodeSuccess, + }) + helpers.Anyhow("rm", "-f", data.Identifier("container-exit")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "create", "--checkpoint-dir", checkpointDir, data.Identifier("container-exit"), checkpointName) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals(checkpointName + "\n"), + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_test.go b/cmd/nerdctl/checkpoint/checkpoint_test.go new file mode 100644 index 00000000000..e32a997e219 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_test.go @@ -0,0 +1,27 @@ +/* + 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 ( + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" +) + +func TestMain(m *testing.M) { + testutil.M(m) +} From 66cfe8da17a32386b9874acd63bb401d21e691f4 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 14 Sep 2025 12:03:18 +0800 Subject: [PATCH 3/4] docs: add checkpoint create command reference add checkpoint create command reference. Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index a3a8979f8de..e08e827d0bb 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -53,6 +53,8 @@ It does not necessarily mean that the corresponding features are missing in cont - [:nerd_face: nerdctl image convert](#nerd_face-nerdctl-image-convert) - [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt) - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) +- [Checkpoint management](#checkpoint-management) + - [:whale: nerdctl checkpoint create](#whale-nerdctl-checkpoint-create) - [Manifest management](#manifest-management) - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) @@ -1060,6 +1062,18 @@ Flags: - `--platform=` : Convert content for a specific platform - `--all-platforms` : Convert content for all platforms (default: false) +## Checkpoint management + +### :whale: nerdctl checkpoint create + +Create a checkpoint from a running container. + +Usage: `nerdctl checkpoint create [OPTIONS] CONTAINER CHECKPOINT` + +Flags: +- :whale: `--leave-running`: Leave the container running after checkpoint +- :whale: `checkpoint-dir`: Use a custom checkpoint storage directory + ## Manifest management ### :whale: nerdctl manifest annotate From 8e2fe266ecec2658e86057da1d7195d33213d323 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 14 Sep 2025 15:48:29 +0800 Subject: [PATCH 4/4] checkpoint: support checkpoint ls command Implement `nerdctl checkpoint ls` command to list checkpoints for a container, matching Docker's output format with "CHECKPOINT NAME" header. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/checkpoint/checkpoint.go | 8 ++ cmd/nerdctl/checkpoint/checkpoint_list.go | 94 +++++++++++++++++++++++ pkg/api/types/checkpoint_types.go | 12 +++ pkg/cmd/checkpoint/list.go | 75 ++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint_list.go create mode 100644 pkg/cmd/checkpoint/list.go 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..b259acf7d04 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_list.go @@ -0,0 +1,94 @@ +/* + 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/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" + "github.com/spf13/cobra" +) + +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/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..e53a99df90a --- /dev/null +++ b/pkg/cmd/checkpoint/list.go @@ -0,0 +1,75 @@ +/* + 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 + } + + if err := os.MkdirAll(checkpointDir, 0o755); 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 +}