From 566925b501fd7dfc4d34c96e47bf9422d32a2b7b 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 | 5 +- pkg/api/types/checkpoint_types.go | 29 +++++ pkg/checkpointutil/checkpointutil.go | 48 +++++++ pkg/cmd/checkpoint/create.go | 135 ++++++++++++++++++++ 8 files changed, 353 insertions(+), 1 deletion(-) 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..a19a0216e4c 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= @@ -161,8 +163,9 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 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 69e76608e8106c39e7dd0dce149208c94ebf773f 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 | 117 ++++++++++++++++++ cmd/nerdctl/checkpoint/checkpoint_test.go | 27 ++++ 2 files changed, 144 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..ca761b586de --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go @@ -0,0 +1,117 @@ +/* + 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/rootlessutil" + "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() + if rootlessutil.IsRootless() { + t.Skip("test skipped for rootless containers") + } + + 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.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+"running") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals(checkpointName + "running\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.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+"exit") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals(checkpointName + "exit\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 8b261f2a64a47250dd691ba463ff9bbad51ee367 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 d611488884c345486511f69aa5a274dd3f7bddbe Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 14 Sep 2025 17:25:52 +0800 Subject: [PATCH 4/4] ci: install criu dependency install criu in ci to test checkpoint. Signed-off-by: ChengyuZhu6 --- .github/workflows/job-test-in-container.yml | 5 +++++ .github/workflows/job-test-in-host.yml | 8 +++++--- .github/workflows/job-test-unit.yml | 7 +++++-- Dockerfile | 17 ++++++++++++----- extras/rootless/containerd-rootless.sh | 14 +++++++++++++- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index be742f8ab00..b1e47e36fb7 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -151,6 +151,11 @@ jobs: sudo mkdir -p /etc/docker echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json sudo systemctl restart docker + - name: "Init: enable Docker experimental features" + run: | + sudo mkdir -p /etc/docker + echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker - name: "Run: integration tests" run: | . ./hack/github/action-helpers.sh diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 8e3b11bdf13..f9ad05066b2 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -107,9 +107,9 @@ jobs: name: "Init (linux): prepare host" run: | if [ "${{ contains(inputs.binary, 'docker') }}" == true ]; then - echo "::group:: configure cdi for docker" + echo "::group:: configure cdi and experimentalfor docker" sudo mkdir -p /etc/docker - sudo jq '.features.cdi = true' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp && sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json + sudo jq -n '.features.cdi = true | .experimental = true' | sudo tee /etc/docker/daemon.json echo "::endgroup::" echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})" sudo apt-get update -qq @@ -122,6 +122,7 @@ jobs: | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -qq sudo apt-get install -qq --allow-downgrades docker-ce=${{ inputs.docker-version }} docker-ce-cli=${{ inputs.docker-version }} + sudo systemctl restart docker echo "::endgroup::" else # FIXME: this is missing runc (see top level workflow note about the state of this) @@ -153,7 +154,8 @@ jobs: # FIXME: remove expect when we are done removing unbuffer from tests echo "::group:: installing test dependencies" - sudo apt-get install -qq expect + sudo add-apt-repository ppa:criu/ppa -y + sudo apt-get install -qq expect criu echo "::endgroup::" # This ensures that bridged traffic goes through netfilter diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 1c7aa9a0069..a7723d88898 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -68,14 +68,17 @@ jobs: go-version: ${{ env.GO_VERSION }} check-latest: true - # Install CNI + # Install CNI and CRIU - if: ${{ env.GO_VERSION != '' }} - name: "Init: set up CNI" + name: "Init: set up CNI and CRIU" run: | if [ "$RUNNER_OS" == "Windows" ]; then GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh elif [ "$RUNNER_OS" == "Linux" ]; then ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" + sudo apt-get update -qq + sudo add-apt-repository ppa:criu/ppa -y + sudo apt-get install -qq criu fi - if: ${{ env.GO_VERSION != '' }} diff --git a/Dockerfile b/Dockerfile index 7f581d28049..62d4b1511ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -294,7 +294,7 @@ RUN perl -pi -e 's/multi-user.target/docker-entrypoint.target/g' /usr/local/lib/ systemctl enable containerd buildkit stargz-snapshotter && \ mkdir -p /etc/bash_completion.d && \ nerdctl completion bash >/etc/bash_completion.d/nerdctl && \ - mkdir -p -m 0755 /etc/cni + mkdir -p -m 0755 /etc/cni /etc/cdi /var/run/cdi /etc/buildkit/cdi COPY ./Dockerfile.d/etc_containerd_config.toml /etc/containerd/config.toml COPY ./Dockerfile.d/etc_buildkit_buildkitd.toml /etc/buildkit/buildkitd.toml VOLUME /var/lib/containerd @@ -309,10 +309,17 @@ ARG DEBIAN_FRONTEND=noninteractive # `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing # `jq` is required to generate test summaries RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ - expect \ - jq \ - git \ - make + software-properties-common \ + gnupg \ + gpg-agent \ + ca-certificates && \ + add-apt-repository ppa:criu/ppa && \ + apt-get update -qq && apt-get install -qq --no-install-recommends \ + expect \ + jq \ + git \ + make \ + criu # We wouldn't need this if Docker Hub could have "golang:${GO_VERSION}-ubuntu" COPY --from=build-base /usr/local/go /usr/local/go ARG TARGETARCH diff --git a/extras/rootless/containerd-rootless.sh b/extras/rootless/containerd-rootless.sh index f569484a574..cafcc7904c6 100755 --- a/extras/rootless/containerd-rootless.sh +++ b/extras/rootless/containerd-rootless.sh @@ -161,7 +161,7 @@ else # so that we can create our own files in our mount namespace. # The actual files in the parent namespace are *not removed* by this rm command. rm -f /run/containerd /run/xtables.lock \ - /var/lib/containerd /var/lib/cni /etc/containerd + /var/lib/containerd /var/lib/cni /etc/containerd /etc/cni/net.d /etc/cdi /var/run/cdi /etc/buildkit/cdi # Bind-mount /etc/ssl. # Workaround for "x509: certificate signed by unknown authority" on openSUSE Tumbleweed. @@ -187,6 +187,18 @@ else mkdir -p "${XDG_CONFIG_HOME}/containerd" "/etc/containerd" mount --bind "${XDG_CONFIG_HOME}/containerd" "/etc/containerd" + # Bind-mount /etc/cni/net.d + mkdir -p "${XDG_CONFIG_HOME}/cni/net.d" "/etc/cni/net.d" + mount --bind "${XDG_CONFIG_HOME}/cni/net.d" "/etc/cni/net.d" + + # Bind-mount CDI directories + mkdir -p "${XDG_CONFIG_HOME}/cdi" "/etc/cdi" + mount --bind "${XDG_CONFIG_HOME}/cdi" "/etc/cdi" + mkdir -p "${XDG_DATA_HOME}/cdi" "/var/run/cdi" + mount --bind "${XDG_DATA_HOME}/cdi" "/var/run/cdi" + mkdir -p "${XDG_CONFIG_HOME}/buildkit/cdi" "/etc/buildkit/cdi" + mount --bind "${XDG_CONFIG_HOME}/buildkit/cdi" "/etc/buildkit/cdi" + if [ -n "$_CONTAINERD_ROOTLESS_SELINUX" ]; then # iptables requires /run in the child to be relabeled. The actual /run in the parent is unaffected. # https://github.com/containers/podman/blob/e6fc34b71aa9d876b1218efe90e14f8b912b0603/libpod/networking_linux.go#L396-L401