Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/nerdctl/checkpoint/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
95 changes: 95 additions & 0 deletions cmd/nerdctl/checkpoint/checkpoint_list.go
Original file line number Diff line number Diff line change
@@ -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)
}
107 changes: 107 additions & 0 deletions cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 10 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions pkg/api/types/checkpoint_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
71 changes: 71 additions & 0 deletions pkg/cmd/checkpoint/list.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading