Skip to content

Commit 2c4729c

Browse files
committed
container: add checkpoint restore support to container start
add checkpoint restore support to container start. e.g.: $ nerdctl run --name cr -d busybox sleep infinity $ nerdctl checkpoint create cr checkpoint1 $ nerdctl start --checkpoint checkpoint cr Signed-off-by: ChengyuZhu6 <[email protected]>
1 parent 9c36d7b commit 2c4729c

File tree

9 files changed

+106
-14
lines changed

9 files changed

+106
-14
lines changed

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)

cmd/nerdctl/container/container_run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ func runAction(cmd *cobra.Command, args []string) error {
432432
logURI := lab[labels.LogURI]
433433
detachC := make(chan struct{})
434434
task, err := taskutil.NewTask(ctx, client, c, createOpt.Attach, createOpt.Interactive, createOpt.TTY, createOpt.Detach,
435-
con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC)
435+
con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC, "")
436436
if err != nil {
437437
return err
438438
}

cmd/nerdctl/container/container_start.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func StartCommand() *cobra.Command {
4444
cmd.Flags().BoolP("attach", "a", false, "Attach STDOUT/STDERR and forward signals")
4545
cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys")
4646
cmd.Flags().BoolP("interactive", "i", false, "Attach container's STDIN")
47+
cmd.Flags().String("checkpoint", "", "checkpoint name")
48+
cmd.Flags().String("checkpoint-dir", "", "checkpoint directory")
4749
return cmd
4850
}
4951

@@ -64,12 +66,22 @@ func startOptions(cmd *cobra.Command) (types.ContainerStartOptions, error) {
6466
if err != nil {
6567
return types.ContainerStartOptions{}, err
6668
}
69+
checkpoint, err := cmd.Flags().GetString("checkpoint")
70+
if err != nil {
71+
return types.ContainerStartOptions{}, err
72+
}
73+
checkpointDir, err := cmd.Flags().GetString("checkpoint-dir")
74+
if err != nil {
75+
return types.ContainerStartOptions{}, err
76+
}
6777
return types.ContainerStartOptions{
68-
Stdout: cmd.OutOrStdout(),
69-
GOptions: globalOptions,
70-
Attach: attach,
71-
DetachKeys: detachKeys,
72-
Interactive: interactive,
78+
Stdout: cmd.OutOrStdout(),
79+
GOptions: globalOptions,
80+
Attach: attach,
81+
DetachKeys: detachKeys,
82+
Interactive: interactive,
83+
Checkpoint: checkpoint,
84+
CheckpointDir: checkpointDir,
7385
}, nil
7486
}
7587

pkg/api/types/container_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ type ContainerStartOptions struct {
3232
DetachKeys string
3333
// Attach stdin
3434
Interactive bool
35+
// Checkpoint is the name of the checkpoint to restore
36+
Checkpoint string
37+
// CheckpointDir is the directory to store checkpoints
38+
CheckpointDir string
3539
}
3640

3741
// ContainerKillOptions specifies options for `nerdctl (container) kill`.

pkg/cmd/checkpoint/create.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"errors"
2323
"fmt"
24+
"path/filepath"
2425

2526
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2627

@@ -97,6 +98,9 @@ func Create(ctx context.Context, client *containerd.Client, containerID string,
9798
return errors.New("invalid checkpoint")
9899
}
99100

101+
if options.CheckpointDir == "" {
102+
options.CheckpointDir = filepath.Join(options.GOptions.DataRoot, "checkpoints")
103+
}
100104
targetPath, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, checkpointName, container.ID(), true)
101105
if err != nil {
102106
return err

pkg/cmd/container/restart.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string
4848
if err := containerutil.Stop(ctx, found.Container, options.Timeout, options.Signal); err != nil {
4949
return err
5050
}
51-
if err := containerutil.Start(ctx, found.Container, false, false, client, "", (*config.Config)(&options.GOption)); err != nil {
51+
if err := containerutil.Start(ctx, found.Container, false, false, client, "", "", (*config.Config)(&options.GOption)); err != nil {
5252
return err
5353
}
5454
_, err = fmt.Fprintln(options.Stdout, found.Req)

pkg/cmd/container/start.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package container
1919
import (
2020
"context"
2121
"fmt"
22+
"path/filepath"
2223

2324
containerd "github.com/containerd/containerd/v2/client"
2425

2526
"github.com/containerd/nerdctl/v2/pkg/api/types"
27+
"github.com/containerd/nerdctl/v2/pkg/checkpointutil"
2628
"github.com/containerd/nerdctl/v2/pkg/config"
2729
"github.com/containerd/nerdctl/v2/pkg/containerutil"
2830
"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
@@ -33,15 +35,28 @@ func Start(ctx context.Context, client *containerd.Client, reqs []string, option
3335
if options.Attach && len(reqs) > 1 {
3436
return fmt.Errorf("you cannot start and attach multiple containers at once")
3537
}
38+
if options.Checkpoint != "" && len(reqs) > 1 {
39+
return fmt.Errorf("you cannot start multiple containers with checkpoint at once")
40+
}
3641

3742
walker := &containerwalker.ContainerWalker{
3843
Client: client,
3944
OnFound: func(ctx context.Context, found containerwalker.Found) error {
4045
var err error
46+
var checkpointDir string
4147
if found.MatchCount > 1 {
4248
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
4349
}
44-
if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys, (*config.Config)(&options.GOptions)); err != nil {
50+
if options.Checkpoint != "" {
51+
if options.CheckpointDir == "" {
52+
options.CheckpointDir = filepath.Join(options.GOptions.DataRoot, "checkpoints")
53+
}
54+
checkpointDir, err = checkpointutil.GetCheckpointDir(options.CheckpointDir, options.Checkpoint, found.Container.ID(), false)
55+
if err != nil {
56+
return err
57+
}
58+
}
59+
if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys, checkpointDir, (*config.Config)(&options.GOptions)); err != nil {
4560
return err
4661
}
4762
if !options.Attach {

pkg/containerutil/containerutil.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container)
206206
}
207207

208208
// Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio.
209-
func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string, cfg *config.Config) (err error) {
209+
func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string, checkpointDir string, cfg *config.Config) (err error) {
210210
// defer the storage of start error in the dedicated label
211211
defer func() {
212212
if err != nil {
@@ -280,7 +280,7 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i
280280
// source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start
281281
attachStreamOpt = []string{"STDOUT", "STDERR"}
282282
}
283-
task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, isInteractive, isTerminal, true, con, logURI, detachKeys, namespace, detachC)
283+
task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, isInteractive, isTerminal, true, con, logURI, detachKeys, namespace, detachC, checkpointDir)
284284
if err != nil {
285285
return err
286286
}

pkg/taskutil/taskutil.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package taskutil
1919
import (
2020
"context"
2121
"errors"
22+
"fmt"
2223
"io"
2324
"net/url"
2425
"os"
@@ -27,13 +28,20 @@ import (
2728
"strings"
2829
"sync"
2930
"syscall"
31+
"time"
3032

3133
"github.com/Masterminds/semver/v3"
34+
"github.com/opencontainers/go-digest"
3235
"golang.org/x/term"
3336

3437
"github.com/containerd/console"
38+
"github.com/containerd/containerd/api/types"
3539
containerd "github.com/containerd/containerd/v2/client"
40+
"github.com/containerd/containerd/v2/core/content"
41+
"github.com/containerd/containerd/v2/core/images"
42+
"github.com/containerd/containerd/v2/pkg/archive"
3643
"github.com/containerd/containerd/v2/pkg/cio"
44+
"github.com/containerd/errdefs"
3745
"github.com/containerd/log"
3846

3947
"github.com/containerd/nerdctl/v2/pkg/cioutil"
@@ -43,9 +51,50 @@ import (
4351

4452
// NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108
4553
func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container,
46-
attachStreamOpt []string, isInteractive, isTerminal, isDetach bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) {
54+
attachStreamOpt []string, isInteractive, isTerminal, isDetach bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}, checkpointDir string) (containerd.Task, error) {
55+
var (
56+
checkpoint *types.Descriptor
57+
t containerd.Task
58+
err error
59+
)
4760

48-
var t containerd.Task
61+
if checkpointDir != "" {
62+
tar := archive.Diff(ctx, "", checkpointDir)
63+
cs := client.ContentStore()
64+
writer, err := cs.Writer(ctx, content.WithRef(checkpointDir))
65+
if err != nil {
66+
return nil, err
67+
}
68+
defer writer.Close()
69+
size, err := io.Copy(writer, tar)
70+
if err != nil {
71+
return nil, err
72+
}
73+
labels := map[string]string{
74+
"containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339),
75+
}
76+
if err = writer.Commit(ctx, size, "", content.WithLabels(labels)); err != nil {
77+
if !errors.Is(err, errdefs.ErrAlreadyExists) {
78+
return nil, err
79+
}
80+
}
81+
checkpoint = &types.Descriptor{
82+
MediaType: images.MediaTypeContainerd1Checkpoint,
83+
Digest: writer.Digest().String(),
84+
Size: size,
85+
}
86+
defer func() {
87+
if checkpoint != nil {
88+
_ = cs.Delete(ctx, digest.Digest(checkpoint.Digest))
89+
}
90+
}()
91+
if err = tar.Close(); err != nil {
92+
return nil, fmt.Errorf("failed to close checkpoint tar stream: %w", err)
93+
}
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to upload checkpoint to containerd: %w", err)
96+
}
97+
}
4998
closer := func() {
5099
if detachC != nil {
51100
detachC <- struct{}{}
@@ -158,7 +207,15 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
158207
}
159208
ioCreator = cioutil.NewContainerIO(namespace, logURI, false, in, os.Stdout, os.Stderr)
160209
}
161-
t, err := container.NewTask(ctx, ioCreator)
210+
211+
taskOpts := []containerd.NewTaskOpts{
212+
func(_ context.Context, _ *containerd.Client, info *containerd.TaskInfo) error {
213+
info.Checkpoint = checkpoint
214+
return nil
215+
},
216+
}
217+
218+
t, err = container.NewTask(ctx, ioCreator, taskOpts...)
162219
if err != nil {
163220
return nil, err
164221
}

0 commit comments

Comments
 (0)