Skip to content

Commit 033f7aa

Browse files
authored
Merge pull request #4484 from ChengyuZhu6/checkpoint
checkpoint: support checkpoint create command
2 parents b439af6 + 627f63d commit 033f7aa

24 files changed

+769
-47
lines changed

.github/workflows/job-test-in-container.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,17 @@ jobs:
153153
sudo sysctl -w net.ipv4.ip_forward=1
154154
# Enable IPv6 for Docker, and configure docker to use containerd for gha
155155
sudo mkdir -p /etc/docker
156-
echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json
156+
echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "ip6tables": true}' | sudo tee /etc/docker/daemon.json
157+
- name: "Init: enable Docker experimental features"
158+
run: |
159+
sudo mkdir -p /etc/docker
160+
if [ -f /etc/docker/daemon.json ]; then
161+
tmpfile="$(sudo mktemp)"
162+
sudo jq '.experimental = true' /etc/docker/daemon.json | sudo tee "$tmpfile" >/dev/null
163+
sudo mv "$tmpfile" /etc/docker/daemon.json
164+
else
165+
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json >/dev/null
166+
fi
157167
sudo systemctl restart docker
158168
- name: "Run: integration tests"
159169
run: |

.github/workflows/job-test-in-host.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ jobs:
107107
name: "Init (linux): prepare host"
108108
run: |
109109
if [ "${{ contains(inputs.binary, 'docker') }}" == true ]; then
110-
echo "::group:: configure cdi for docker"
110+
echo "::group:: configure cdi and experimental for docker"
111111
sudo mkdir -p /etc/docker
112-
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
112+
sudo jq -n '.features.cdi = true | .experimental = true' | sudo tee /etc/docker/daemon.json
113113
echo "::endgroup::"
114114
echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})"
115115
sudo apt-get update -qq
@@ -122,6 +122,7 @@ jobs:
122122
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
123123
sudo apt-get update -qq
124124
sudo apt-get install -qq --allow-downgrades docker-ce=${{ inputs.docker-version }} docker-ce-cli=${{ inputs.docker-version }}
125+
sudo systemctl restart docker
125126
echo "::endgroup::"
126127
else
127128
# FIXME: this is missing runc (see top level workflow note about the state of this)
@@ -153,7 +154,8 @@ jobs:
153154
154155
# FIXME: remove expect when we are done removing unbuffer from tests
155156
echo "::group:: installing test dependencies"
156-
sudo apt-get install -qq expect
157+
sudo add-apt-repository ppa:criu/ppa -y
158+
sudo apt-get install -qq expect criu
157159
echo "::endgroup::"
158160
159161
# This ensures that bridged traffic goes through netfilter

.github/workflows/job-test-unit.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,17 @@ jobs:
6868
go-version: ${{ env.GO_VERSION }}
6969
check-latest: true
7070

71-
# Install CNI
71+
# Install CNI and CRIU
7272
- if: ${{ env.GO_VERSION != '' }}
73-
name: "Init: set up CNI"
73+
name: "Init: set up CNI and CRIU"
7474
run: |
7575
if [ "$RUNNER_OS" == "Windows" ]; then
7676
GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh
7777
elif [ "$RUNNER_OS" == "Linux" ]; then
7878
./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}"
79+
sudo apt-get update -qq
80+
sudo add-apt-repository ppa:criu/ppa -y
81+
sudo apt-get install -qq criu
7982
fi
8083
8184
- if: ${{ env.GO_VERSION != '' }}

Dockerfile

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,17 @@ ARG DEBIAN_FRONTEND=noninteractive
309309
# `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing
310310
# `jq` is required to generate test summaries
311311
RUN apt-get update -qq && apt-get install -qq --no-install-recommends \
312-
expect \
313-
jq \
314-
git \
315-
make
312+
software-properties-common \
313+
gnupg \
314+
gpg-agent \
315+
ca-certificates && \
316+
add-apt-repository ppa:criu/ppa && \
317+
apt-get update -qq && apt-get install -qq --no-install-recommends \
318+
expect \
319+
jq \
320+
git \
321+
make \
322+
criu
316323
# We wouldn't need this if Docker Hub could have "golang:${GO_VERSION}-ubuntu"
317324
COPY --from=build-base /usr/local/go /usr/local/go
318325
ARG TARGETARCH
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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/spf13/cobra"
21+
22+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
23+
)
24+
25+
func Command() *cobra.Command {
26+
cmd := &cobra.Command{
27+
Annotations: map[string]string{helpers.Category: helpers.Management},
28+
Use: "checkpoint",
29+
Short: "Manage checkpoints.",
30+
RunE: helpers.UnknownSubcommandAction,
31+
SilenceUsage: true,
32+
SilenceErrors: true,
33+
}
34+
35+
cmd.AddCommand(
36+
CreateCommand(),
37+
)
38+
39+
return cmd
40+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
"path/filepath"
21+
22+
"github.com/spf13/cobra"
23+
24+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
25+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
26+
"github.com/containerd/nerdctl/v2/pkg/api/types"
27+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
28+
"github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint"
29+
)
30+
31+
func CreateCommand() *cobra.Command {
32+
var cmd = &cobra.Command{
33+
Use: "create [OPTIONS] CONTAINER CHECKPOINT",
34+
Short: "Create a checkpoint from a running container",
35+
Args: cobra.ExactArgs(2),
36+
RunE: createAction,
37+
ValidArgsFunction: createShellComplete,
38+
SilenceUsage: true,
39+
SilenceErrors: true,
40+
}
41+
cmd.Flags().Bool("leave-running", false, "Leave the container running after checkpointing")
42+
cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory")
43+
return cmd
44+
}
45+
46+
func processCreateFlags(cmd *cobra.Command) (types.CheckpointCreateOptions, error) {
47+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
48+
if err != nil {
49+
return types.CheckpointCreateOptions{}, err
50+
}
51+
52+
leaveRunning, err := cmd.Flags().GetBool("leave-running")
53+
if err != nil {
54+
return types.CheckpointCreateOptions{}, err
55+
}
56+
checkpointDir, err := cmd.Flags().GetString("checkpoint-dir")
57+
if err != nil {
58+
return types.CheckpointCreateOptions{}, err
59+
}
60+
if checkpointDir == "" {
61+
checkpointDir = filepath.Join(globalOptions.DataRoot, "checkpoints")
62+
}
63+
64+
return types.CheckpointCreateOptions{
65+
Stdout: cmd.OutOrStdout(),
66+
GOptions: globalOptions,
67+
LeaveRunning: leaveRunning,
68+
CheckpointDir: checkpointDir,
69+
}, nil
70+
}
71+
72+
func createAction(cmd *cobra.Command, args []string) error {
73+
createOptions, err := processCreateFlags(cmd)
74+
if err != nil {
75+
return err
76+
}
77+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), createOptions.GOptions.Namespace, createOptions.GOptions.Address)
78+
if err != nil {
79+
return err
80+
}
81+
defer cancel()
82+
83+
err = checkpoint.Create(ctx, client, args[0], args[1], createOptions)
84+
if err != nil {
85+
return err
86+
}
87+
88+
return nil
89+
}
90+
91+
func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
92+
return completion.ImageNames(cmd)
93+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
"errors"
21+
"testing"
22+
23+
"github.com/containerd/nerdctl/mod/tigron/expect"
24+
"github.com/containerd/nerdctl/mod/tigron/require"
25+
"github.com/containerd/nerdctl/mod/tigron/test"
26+
27+
"github.com/containerd/nerdctl/v2/pkg/testutil"
28+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
29+
)
30+
31+
func TestCheckpointCreateErrors(t *testing.T) {
32+
testCase := nerdtest.Setup()
33+
34+
testCase.Require = require.All(
35+
require.Not(nerdtest.Rootless),
36+
// Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality.
37+
// The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750.
38+
require.Not(nerdtest.Docker),
39+
)
40+
testCase.SubTests = []*test.Case{
41+
{
42+
Description: "too-few-arguments",
43+
Command: test.Command("checkpoint", "create", "too-few-arguments"),
44+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
45+
return &test.Expected{
46+
ExitCode: 1,
47+
}
48+
},
49+
},
50+
{
51+
Description: "too-many-arguments",
52+
Command: test.Command("checkpoint", "create", "too", "many", "arguments"),
53+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
54+
return &test.Expected{
55+
ExitCode: 1,
56+
}
57+
},
58+
},
59+
{
60+
Description: "invalid-container-id",
61+
Command: test.Command("checkpoint", "create", "foo", "bar"),
62+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
63+
return &test.Expected{
64+
ExitCode: 1,
65+
Errors: []error{errors.New("error creating checkpoint for container: foo")},
66+
}
67+
},
68+
},
69+
}
70+
71+
testCase.Run(t)
72+
}
73+
74+
func TestCheckpointCreate(t *testing.T) {
75+
const (
76+
checkpointName = "checkpoint-bar"
77+
checkpointDir = "/dir/foo"
78+
)
79+
testCase := nerdtest.Setup()
80+
testCase.Require = require.All(
81+
require.Not(nerdtest.Rootless),
82+
// Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality.
83+
// The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750.
84+
require.Not(nerdtest.Docker),
85+
)
86+
testCase.SubTests = []*test.Case{
87+
{
88+
Description: "leave-running=true",
89+
Setup: func(data test.Data, helpers test.Helpers) {
90+
helpers.Ensure("run", "-d", "--name", data.Identifier("container-running"), testutil.CommonImage, "sleep", "infinity")
91+
},
92+
Cleanup: func(data test.Data, helpers test.Helpers) {
93+
helpers.Anyhow("rm", "-f", data.Identifier("container-running"))
94+
},
95+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
96+
return helpers.Command("checkpoint", "create", "--leave-running", "--checkpoint-dir", checkpointDir, data.Identifier("container-running"), checkpointName+"running")
97+
},
98+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
99+
return &test.Expected{
100+
ExitCode: 0,
101+
Output: expect.Equals(checkpointName + "running\n"),
102+
}
103+
},
104+
},
105+
{
106+
Description: "leave-running=false",
107+
Setup: func(data test.Data, helpers test.Helpers) {
108+
helpers.Ensure("run", "-d", "--name", data.Identifier("container-exit"), testutil.CommonImage, "sleep", "infinity")
109+
},
110+
Cleanup: func(data test.Data, helpers test.Helpers) {
111+
helpers.Anyhow("rm", "-f", data.Identifier("container-exit"))
112+
},
113+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
114+
return helpers.Command("checkpoint", "create", "--checkpoint-dir", checkpointDir, data.Identifier("container-exit"), checkpointName+"exit")
115+
},
116+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
117+
return &test.Expected{
118+
ExitCode: 0,
119+
Output: expect.Equals(checkpointName + "exit\n"),
120+
}
121+
},
122+
},
123+
}
124+
125+
testCase.Run(t)
126+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
"testing"
21+
22+
"github.com/containerd/nerdctl/v2/pkg/testutil"
23+
)
24+
25+
func TestMain(m *testing.M) {
26+
testutil.M(m)
27+
}

cmd/nerdctl/compose/compose_start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func startContainers(ctx context.Context, client *containerd.Client, containers
114114
}
115115

116116
// in compose, always disable attach
117-
if err := containerutil.Start(ctx, c, false, false, client, "", (*config.Config)(globalOptions)); err != nil {
117+
if err := containerutil.Start(ctx, c, false, false, client, "", "", (*config.Config)(globalOptions)); err != nil {
118118
return err
119119
}
120120
info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata)

0 commit comments

Comments
 (0)