From 45c700a95584594e869251178a88d751a5e3295b Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 10 Sep 2018 17:16:52 +0000 Subject: [PATCH 1/9] refactor checkpoint and restore to client Signed-off-by: Evan Hazlett --- client.go | 105 ++++++++++++++++++ cmd/ctr/commands/containers/containers.go | 95 ++++++++++++++++ container.go | 100 +++++++++++++++++ container_checkpoint_opts.go | 125 ++++++++++++++++++++++ container_restore_opts.go | 95 ++++++++++++++++ images/mediatypes.go | 11 +- 6 files changed, 526 insertions(+), 5 deletions(-) create mode 100644 container_checkpoint_opts.go create mode 100644 container_restore_opts.go diff --git a/client.go b/client.go index fd20c3dd0..d13509b77 100644 --- a/client.go +++ b/client.go @@ -17,7 +17,9 @@ package containerd import ( + "bytes" "context" + "encoding/json" "fmt" "net/http" "runtime" @@ -36,6 +38,7 @@ import ( snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" "github.com/containerd/containerd/api/services/tasks/v1" versionservice "github.com/containerd/containerd/api/services/version/v1" + "github.com/containerd/containerd/cio" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" contentproxy "github.com/containerd/containerd/content/proxy" @@ -46,6 +49,7 @@ import ( "github.com/containerd/containerd/leases" leasesproxy "github.com/containerd/containerd/leases/proxy" "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/oci" "github.com/containerd/containerd/pkg/dialer" "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/plugin" @@ -520,6 +524,107 @@ func (c *Client) ListImages(ctx context.Context, filters ...string) ([]Image, er return images, nil } +// Restore restores a container from a checkpoint +func (c *Client) Restore(ctx context.Context, id, ref string, opts ...RestoreOpts) error { + checkpoint, err := c.GetImage(ctx, ref) + if err != nil { + if !errdefs.IsNotFound(err) { + return err + } + ck, err := c.Fetch(ctx, ref) + if err != nil { + return err + } + checkpoint = NewImage(c, ck) + } + + store := c.ContentStore() + index, err := decodeIndex(ctx, store, checkpoint.Target()) + if err != nil { + return err + } + + // get image from annotation + imageName, ok := index.Annotations["image.name"] + if !ok { + return ErrCheckpointIndexImageNameNotFound + } + + image, err := c.Pull(ctx, imageName, WithPullUnpack) + if err != nil { + return err + } + + ctx, done, err := c.WithLease(ctx) + if err != nil { + return err + } + defer done(ctx) + + // container options + copts := []NewContainerOpts{ + WithNewSpec(oci.WithImageConfig(image)), + WithNewSnapshot(id, image), + } + topts := []NewTaskOpts{} + for _, o := range opts { + co, to, err := o(ctx, c, checkpoint, index) + if err != nil { + return err + } + copts = append(copts, co...) + topts = append(topts, to...) + } + + ctr, err := c.NewContainer(ctx, id, copts...) + if err != nil { + return err + } + + // apply rw layer + info, err := ctr.Info(ctx) + if err != nil { + return err + } + + rw, err := GetIndexByMediaType(index, ocispec.MediaTypeImageLayerGzip) + if err != nil { + return err + } + + mounts, err := c.SnapshotService(info.Snapshotter).Mounts(ctx, info.SnapshotKey) + if err != nil { + return err + } + + if _, err := c.DiffService().Apply(ctx, *rw, mounts); err != nil { + return err + } + + task, err := ctr.NewTask(ctx, cio.NewCreator(cio.WithStdio), topts...) + if err != nil { + return err + } + + if err := task.Start(ctx); err != nil { + return err + } + + return nil +} + +func writeIndex(ctx context.Context, index *ocispec.Index, client *Client, ref string) (d ocispec.Descriptor, err error) { + labels := map[string]string{} + for i, m := range index.Manifests { + labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = m.Digest.String() + } + data, err := json.Marshal(index) + if err != nil { + return ocispec.Descriptor{}, err + } + return writeContent(ctx, client.ContentStore(), ocispec.MediaTypeImageIndex, ref, bytes.NewReader(data), content.WithLabels(labels)) +} + // Subscribe to events that match one or more of the provided filters. // // Callers should listen on both the envelope and errs channels. If the errs diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index 0b88c6254..0b1d4dbbd 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -45,6 +45,8 @@ var Command = cli.Command{ infoCommand, listCommand, setLabelsCommand, + checkpointCommand, + restoreCommand, }, } @@ -282,3 +284,96 @@ var infoCommand = cli.Command{ return nil }, } + +var checkpointCommand = cli.Command{ + Name: "checkpoint", + Usage: "checkpoint a container", + ArgsUsage: "CONTAINER REF [flags]", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "rw", + Usage: "include the rw layer in the checkpoint", + }, + cli.BoolFlag{ + Name: "image", + Usage: "include the image in the checkpoint", + }, + cli.BoolFlag{ + Name: "task", + Usage: "checkpoint container task", + }, + }, + Action: func(context *cli.Context) error { + id := context.Args().First() + if id == "" { + return errors.New("container id must be provided") + } + ref := context.Args()[1] + if ref == "" { + return errors.New("ref must be provided") + } + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + opts := []containerd.CheckpointOpts{} + if context.Bool("image") { + opts = append(opts, containerd.WithCheckpointImage) + } + if context.Bool("rw") { + opts = append(opts, containerd.WithCheckpointRW) + } + if context.Bool("task") { + opts = append(opts, containerd.WithCheckpointTask) + } + container, err := client.LoadContainer(ctx, id) + if err != nil { + return err + } + if _, err := container.Checkpoint(ctx, ref, opts...); err != nil { + return err + } + + return nil + }, +} + +var restoreCommand = cli.Command{ + Name: "restore", + Usage: "restore a container from checkpoint", + ArgsUsage: "CONTAINER REF [flags]", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "live", + Usage: "restore the runtime and memory data from the checkpoint", + }, + }, + Action: func(context *cli.Context) error { + id := context.Args().First() + if id == "" { + return errors.New("container id must be provided") + } + ref := context.Args()[1] + if ref == "" { + return errors.New("ref must be provided") + } + opts := []containerd.RestoreOpts{ + containerd.WithRestoreSpec, + containerd.WithRestoreRuntime, + } + if context.Bool("live") { + opts = append(opts, containerd.WithRestoreLive) + } + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + if err := client.Restore(ctx, id, ref, opts...); err != nil { + return err + } + + return nil + }, +} diff --git a/container.go b/container.go index 3c09b2dbc..4d24adfc3 100644 --- a/container.go +++ b/container.go @@ -17,10 +17,12 @@ package containerd import ( + "bytes" "context" "encoding/json" "os" "path/filepath" + "runtime" "strings" "github.com/containerd/containerd/api/services/tasks/v1" @@ -28,9 +30,13 @@ import ( "github.com/containerd/containerd/cio" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/runtime/v2/runc/options" "github.com/containerd/typeurl" prototypes "github.com/gogo/protobuf/types" + ver "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) @@ -64,6 +70,8 @@ type Container interface { Extensions(context.Context) (map[string]prototypes.Any, error) // Update a container Update(context.Context, ...UpdateContainerOpts) error + // Checkpoint creates a checkpoint image of the current container + Checkpoint(context.Context, string, ...CheckpointOpts) (Image, error) } func containerFromRecord(client *Client, c containers.Container) *container { @@ -272,6 +280,98 @@ func (c *container) Update(ctx context.Context, opts ...UpdateContainerOpts) err return nil } +func (c *container) Checkpoint(ctx context.Context, ref string, opts ...CheckpointOpts) (Image, error) { + index := &ocispec.Index{ + Versioned: ver.Versioned{ + SchemaVersion: 2, + }, + Annotations: make(map[string]string), + } + copts := &options.CheckpointOptions{ + Exit: false, + OpenTcp: false, + ExternalUnixSockets: false, + Terminal: false, + FileLocks: true, + EmptyNamespaces: nil, + } + img, err := c.Image(ctx) + if err != nil { + return nil, err + } + index.Annotations["image.name"] = img.Name() + + ctx, done, err := c.client.WithLease(ctx) + if err != nil { + return nil, err + } + defer done(ctx) + + // pause task to checkpoint + if err := func(ctx context.Context) error { + task, err := c.Task(ctx, nil) + if err != nil { + if errdefs.IsNotFound(err) { + return nil + } + return err + } + if err := task.Pause(ctx); err != nil { + return err + } + defer task.Resume(ctx) + + info, err := c.Info(ctx) + if err != nil { + return err + } + + // add runtime info to index + index.Annotations["runtime.name"] = info.Runtime.Name + if info.Runtime.Options != nil { + data, err := info.Runtime.Options.Marshal() + if err != nil { + return err + } + r := bytes.NewReader(data) + desc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeOptions, info.ID+"-runtime-options", r) + if err != nil { + return err + } + desc.Platform = &ocispec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + index.Manifests = append(index.Manifests, desc) + } + + // process remaining opts + for _, o := range opts { + if err := o(ctx, c.client, &info, index, copts); err != nil { + return err + } + } + return nil + }(ctx); err != nil { + return nil, err + } + + desc, err := writeIndex(ctx, index, c.client, c.ID()+"index") + if err != nil { + return nil, err + } + i := images.Image{ + Name: ref, + Target: desc, + } + checkpoint, err := c.client.ImageService().Create(ctx, i) + if err != nil { + return nil, err + } + + return NewImage(c.client, checkpoint), nil +} + func (c *container) loadTask(ctx context.Context, ioAttach cio.Attach) (Task, error) { response, err := c.client.TaskService().Get(ctx, &tasks.GetRequest{ ContainerID: c.id, diff --git a/container_checkpoint_opts.go b/container_checkpoint_opts.go new file mode 100644 index 000000000..0af5f8ce2 --- /dev/null +++ b/container_checkpoint_opts.go @@ -0,0 +1,125 @@ +/* + 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 containerd + +import ( + "context" + "fmt" + "runtime" + + tasks "github.com/containerd/containerd/api/services/tasks/v1" + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/diff" + "github.com/containerd/containerd/rootfs" + "github.com/containerd/containerd/runtime/v2/runc/options" + "github.com/containerd/typeurl" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +var ( + // ErrCheckpointRWUnsupported is returned if the container runtime does not support checkpoint + ErrCheckpointRWUnsupported = errors.New("rw checkpoint is only supported on v2 runtimes") + // ErrMediaTypeNotFound returns an error when a media type in the manifest is unknown + ErrMediaTypeNotFound = errors.New("media type not found") + // ErrCheckpointIndexImageNameNotFound is returned when the checkpoint image name is not present in the index + ErrCheckpointIndexImageNameNotFound = errors.New("image name not present in index") + // ErrCheckpointIndexRuntimeNameNotFound is returned when the checkpoint runtime name is not present in the index + ErrCheckpointIndexRuntimeNameNotFound = errors.New("runtime name not present in index") +) + +// CheckpointOpts are options to manage the checkpoint operation +type CheckpointOpts func(context.Context, *Client, *containers.Container, *imagespec.Index, *options.CheckpointOptions) error + +// WithCheckpointImage includes the container image in the checkpoint +func WithCheckpointImage(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error { + ir, err := client.ImageService().Get(ctx, c.Image) + if err != nil { + return err + } + index.Manifests = append(index.Manifests, ir.Target) + return nil +} + +// WithCheckpointTask includes the running task +func WithCheckpointTask(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error { + any, err := typeurl.MarshalAny(copts) + if err != nil { + return nil + } + task, err := client.TaskService().Checkpoint(ctx, &tasks.CheckpointTaskRequest{ + ContainerID: c.ID, + Options: any, + }) + if err != nil { + return err + } + for _, d := range task.Descriptors { + index.Manifests = append(index.Manifests, imagespec.Descriptor{ + MediaType: d.MediaType, + Size: d.Size_, + Digest: d.Digest, + Platform: &imagespec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + }, + }) + } + return nil +} + +// WithCheckpointRW includes the rw in the checkpoint +func WithCheckpointRW(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error { + cnt, err := client.LoadContainer(ctx, c.ID) + if err != nil { + return err + } + info, err := cnt.Info(ctx) + if err != nil { + return err + } + + diffOpts := []diff.Opt{ + diff.WithReference(fmt.Sprintf("checkpoint-rw-%s", info.SnapshotKey)), + } + rw, err := rootfs.CreateDiff(ctx, + info.SnapshotKey, + client.SnapshotService(info.Snapshotter), + client.DiffService(), + diffOpts..., + ) + if err != nil { + return err + + } + rw.Platform = &imagespec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + index.Manifests = append(index.Manifests, rw) + return nil +} + +// GetIndexByMediaType returns the index in a manifest for the specified media type +func GetIndexByMediaType(index *imagespec.Index, mt string) (*imagespec.Descriptor, error) { + for _, d := range index.Manifests { + if d.MediaType == mt { + return &d, nil + } + } + return nil, ErrMediaTypeNotFound +} diff --git a/container_restore_opts.go b/container_restore_opts.go new file mode 100644 index 000000000..52c343ec6 --- /dev/null +++ b/container_restore_opts.go @@ -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 containerd + +import ( + "context" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/oci" + "github.com/containerd/typeurl" + "github.com/gogo/protobuf/proto" + ptypes "github.com/gogo/protobuf/types" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +// RestoreOpts are options to manage the restore operation +type RestoreOpts func(context.Context, *Client, Image, *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) + +// WithRestoreLive restores the runtime and memory data for the container +func WithRestoreLive(ctx context.Context, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { + return nil, []NewTaskOpts{ + WithTaskCheckpoint(checkpoint), + }, nil +} + +// WithRestoreRuntime restores the runtime for the container +func WithRestoreRuntime(ctx context.Context, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { + runtimeName, ok := index.Annotations["runtime.name"] + if !ok { + return nil, nil, ErrCheckpointIndexRuntimeNameNotFound + } + // restore options if present + m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeOptions) + if err != nil { + if err != ErrMediaTypeNotFound { + return nil, nil, err + } + } + var options *ptypes.Any + if m != nil { + store := client.ContentStore() + data, err := content.ReadBlob(ctx, store, *m) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to read checkpoint runtime") + } + if err := proto.Unmarshal(data, options); err != nil { + return nil, nil, err + } + } + return []NewContainerOpts{ + WithRuntime(runtimeName, options), + }, nil, nil +} + +// WithRestoreSpec restores the spec from the checkpoint for the container +func WithRestoreSpec(ctx context.Context, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { + m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointConfig) + if err != nil { + return nil, nil, err + } + store := client.ContentStore() + data, err := content.ReadBlob(ctx, store, *m) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to read checkpoint config") + } + var any ptypes.Any + if err := proto.Unmarshal(data, &any); err != nil { + return nil, nil, err + } + + v, err := typeurl.UnmarshalAny(&any) + if err != nil { + return nil, nil, err + } + spec := v.(*oci.Spec) + return []NewContainerOpts{ + WithSpec(spec), + }, nil, nil +} diff --git a/images/mediatypes.go b/images/mediatypes.go index ca4ca071b..e7a84526f 100644 --- a/images/mediatypes.go +++ b/images/mediatypes.go @@ -29,11 +29,12 @@ const ( MediaTypeDockerSchema2Manifest = "application/vnd.docker.distribution.manifest.v2+json" MediaTypeDockerSchema2ManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" // Checkpoint/Restore Media Types - MediaTypeContainerd1Checkpoint = "application/vnd.containerd.container.criu.checkpoint.criu.tar" - MediaTypeContainerd1CheckpointPreDump = "application/vnd.containerd.container.criu.checkpoint.predump.tar" - MediaTypeContainerd1Resource = "application/vnd.containerd.container.resource.tar" - MediaTypeContainerd1RW = "application/vnd.containerd.container.rw.tar" - MediaTypeContainerd1CheckpointConfig = "application/vnd.containerd.container.checkpoint.config.v1+proto" + MediaTypeContainerd1Checkpoint = "application/vnd.containerd.container.criu.checkpoint.criu.tar" + MediaTypeContainerd1CheckpointPreDump = "application/vnd.containerd.container.criu.checkpoint.predump.tar" + MediaTypeContainerd1Resource = "application/vnd.containerd.container.resource.tar" + MediaTypeContainerd1RW = "application/vnd.containerd.container.rw.tar" + MediaTypeContainerd1CheckpointConfig = "application/vnd.containerd.container.checkpoint.config.v1+proto" + MediaTypeContainerd1CheckpointRuntimeOptions = "application/vnd.containerd.container.checkpoint.runtime.options+proto" // Legacy Docker schema1 manifest MediaTypeDockerSchema1Manifest = "application/vnd.docker.distribution.manifest.v1+prettyjws" ) From 2d3db08daf5169c060020b57368e66b74735b02f Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 17 Sep 2018 09:47:04 -0400 Subject: [PATCH 2/9] refactor spec and snapshot restore into opts Signed-off-by: Evan Hazlett --- client.go | 20 ++--------- cmd/ctr/commands/containers/containers.go | 20 ++++++----- container_checkpoint_opts.go | 4 --- container_restore_opts.go | 42 ++++++++++++++++++++--- 4 files changed, 51 insertions(+), 35 deletions(-) diff --git a/client.go b/client.go index d13509b77..0301eeb0d 100644 --- a/client.go +++ b/client.go @@ -49,7 +49,6 @@ import ( "github.com/containerd/containerd/leases" leasesproxy "github.com/containerd/containerd/leases/proxy" "github.com/containerd/containerd/namespaces" - "github.com/containerd/containerd/oci" "github.com/containerd/containerd/pkg/dialer" "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/plugin" @@ -544,31 +543,16 @@ func (c *Client) Restore(ctx context.Context, id, ref string, opts ...RestoreOpt return err } - // get image from annotation - imageName, ok := index.Annotations["image.name"] - if !ok { - return ErrCheckpointIndexImageNameNotFound - } - - image, err := c.Pull(ctx, imageName, WithPullUnpack) - if err != nil { - return err - } - ctx, done, err := c.WithLease(ctx) if err != nil { return err } defer done(ctx) - // container options - copts := []NewContainerOpts{ - WithNewSpec(oci.WithImageConfig(image)), - WithNewSnapshot(id, image), - } + copts := []NewContainerOpts{} topts := []NewTaskOpts{} for _, o := range opts { - co, to, err := o(ctx, c, checkpoint, index) + co, to, err := o(ctx, id, c, checkpoint, index) if err != nil { return err } diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index 0b1d4dbbd..72b0beae8 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -288,7 +288,7 @@ var infoCommand = cli.Command{ var checkpointCommand = cli.Command{ Name: "checkpoint", Usage: "checkpoint a container", - ArgsUsage: "CONTAINER REF [flags]", + ArgsUsage: "CONTAINER REF", Flags: []cli.Flag{ cli.BoolFlag{ Name: "rw", @@ -342,7 +342,7 @@ var checkpointCommand = cli.Command{ var restoreCommand = cli.Command{ Name: "restore", Usage: "restore a container from checkpoint", - ArgsUsage: "CONTAINER REF [flags]", + ArgsUsage: "CONTAINER REF", Flags: []cli.Flag{ cli.BoolFlag{ Name: "live", @@ -358,18 +358,20 @@ var restoreCommand = cli.Command{ if ref == "" { return errors.New("ref must be provided") } - opts := []containerd.RestoreOpts{ - containerd.WithRestoreSpec, - containerd.WithRestoreRuntime, - } - if context.Bool("live") { - opts = append(opts, containerd.WithRestoreLive) - } client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() + + opts := []containerd.RestoreOpts{ + containerd.WithRestoreSpec, + containerd.WithRestoreSnapshot, + containerd.WithRestoreRuntime, + } + if context.Bool("live") { + opts = append(opts, containerd.WithRestoreLive) + } if err := client.Restore(ctx, id, ref, opts...); err != nil { return err } diff --git a/container_checkpoint_opts.go b/container_checkpoint_opts.go index 0af5f8ce2..b0d618070 100644 --- a/container_checkpoint_opts.go +++ b/container_checkpoint_opts.go @@ -36,10 +36,6 @@ var ( ErrCheckpointRWUnsupported = errors.New("rw checkpoint is only supported on v2 runtimes") // ErrMediaTypeNotFound returns an error when a media type in the manifest is unknown ErrMediaTypeNotFound = errors.New("media type not found") - // ErrCheckpointIndexImageNameNotFound is returned when the checkpoint image name is not present in the index - ErrCheckpointIndexImageNameNotFound = errors.New("image name not present in index") - // ErrCheckpointIndexRuntimeNameNotFound is returned when the checkpoint runtime name is not present in the index - ErrCheckpointIndexRuntimeNameNotFound = errors.New("runtime name not present in index") ) // CheckpointOpts are options to manage the checkpoint operation diff --git a/container_restore_opts.go b/container_restore_opts.go index 52c343ec6..3b23bc57f 100644 --- a/container_restore_opts.go +++ b/container_restore_opts.go @@ -22,25 +22,34 @@ import ( "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/platforms" "github.com/containerd/typeurl" "github.com/gogo/protobuf/proto" ptypes "github.com/gogo/protobuf/types" + "github.com/opencontainers/image-spec/identity" imagespec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) +var ( + // ErrCheckpointIndexImageNameNotFound is returned when the checkpoint image name is not present in the index + ErrCheckpointIndexImageNameNotFound = errors.New("image name not present in index") + // ErrCheckpointIndexRuntimeNameNotFound is returned when the checkpoint runtime name is not present in the index + ErrCheckpointIndexRuntimeNameNotFound = errors.New("runtime name not present in index") +) + // RestoreOpts are options to manage the restore operation -type RestoreOpts func(context.Context, *Client, Image, *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) +type RestoreOpts func(context.Context, string, *Client, Image, *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) // WithRestoreLive restores the runtime and memory data for the container -func WithRestoreLive(ctx context.Context, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { +func WithRestoreLive(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { return nil, []NewTaskOpts{ WithTaskCheckpoint(checkpoint), }, nil } // WithRestoreRuntime restores the runtime for the container -func WithRestoreRuntime(ctx context.Context, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { +func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { runtimeName, ok := index.Annotations["runtime.name"] if !ok { return nil, nil, ErrCheckpointIndexRuntimeNameNotFound @@ -69,7 +78,7 @@ func WithRestoreRuntime(ctx context.Context, client *Client, checkpoint Image, i } // WithRestoreSpec restores the spec from the checkpoint for the container -func WithRestoreSpec(ctx context.Context, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { +func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointConfig) if err != nil { return nil, nil, err @@ -93,3 +102,28 @@ func WithRestoreSpec(ctx context.Context, client *Client, checkpoint Image, inde WithSpec(spec), }, nil, nil } + +func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { + // get image from annotation + imageName, ok := index.Annotations["image.name"] + if !ok { + return nil, nil, ErrCheckpointIndexImageNameNotFound + } + i, err := client.Pull(ctx, imageName, WithPullUnpack) + if err != nil { + return nil, nil, err + } + diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) + if err != nil { + return nil, nil, err + } + //setSnapshotterIfEmpty(client) + parent := identity.ChainID(diffIDs).String() + if _, err := client.SnapshotService(DefaultSnapshotter).Prepare(ctx, id, parent); err != nil { + return nil, nil, err + } + return []NewContainerOpts{ + WithImage(i), + WithSnapshot(id), + }, nil, nil +} From 0e4d9da755809ed4a8fbd7d9782d5f89c612612b Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 17 Sep 2018 11:04:13 -0400 Subject: [PATCH 3/9] remove task handling from Restore Signed-off-by: Evan Hazlett --- client.go | 38 +++++++----------- cmd/ctr/commands/containers/containers.go | 31 +++++++++++++-- container_restore_opts.go | 47 ++++++++++------------- 3 files changed, 61 insertions(+), 55 deletions(-) diff --git a/client.go b/client.go index 0301eeb0d..01c1c2441 100644 --- a/client.go +++ b/client.go @@ -38,7 +38,6 @@ import ( snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" "github.com/containerd/containerd/api/services/tasks/v1" versionservice "github.com/containerd/containerd/api/services/version/v1" - "github.com/containerd/containerd/cio" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" contentproxy "github.com/containerd/containerd/content/proxy" @@ -524,15 +523,15 @@ func (c *Client) ListImages(ctx context.Context, filters ...string) ([]Image, er } // Restore restores a container from a checkpoint -func (c *Client) Restore(ctx context.Context, id, ref string, opts ...RestoreOpts) error { +func (c *Client) Restore(ctx context.Context, id, ref string, opts ...RestoreOpts) (Container, error) { checkpoint, err := c.GetImage(ctx, ref) if err != nil { if !errdefs.IsNotFound(err) { - return err + return nil, err } ck, err := c.Fetch(ctx, ref) if err != nil { - return err + return nil, err } checkpoint = NewImage(c, ck) } @@ -540,61 +539,50 @@ func (c *Client) Restore(ctx context.Context, id, ref string, opts ...RestoreOpt store := c.ContentStore() index, err := decodeIndex(ctx, store, checkpoint.Target()) if err != nil { - return err + return nil, err } ctx, done, err := c.WithLease(ctx) if err != nil { - return err + return nil, err } defer done(ctx) copts := []NewContainerOpts{} - topts := []NewTaskOpts{} for _, o := range opts { - co, to, err := o(ctx, id, c, checkpoint, index) + co, err := o(ctx, id, c, checkpoint, index) if err != nil { - return err + return nil, err } copts = append(copts, co...) - topts = append(topts, to...) } ctr, err := c.NewContainer(ctx, id, copts...) if err != nil { - return err + return nil, err } // apply rw layer info, err := ctr.Info(ctx) if err != nil { - return err + return nil, err } rw, err := GetIndexByMediaType(index, ocispec.MediaTypeImageLayerGzip) if err != nil { - return err + return nil, err } mounts, err := c.SnapshotService(info.Snapshotter).Mounts(ctx, info.SnapshotKey) if err != nil { - return err + return nil, err } if _, err := c.DiffService().Apply(ctx, *rw, mounts); err != nil { - return err + return nil, err } - task, err := ctr.NewTask(ctx, cio.NewCreator(cio.WithStdio), topts...) - if err != nil { - return err - } - - if err := task.Start(ctx); err != nil { - return err - } - - return nil + return ctr, nil } func writeIndex(ctx context.Context, index *ocispec.Index, client *Client, ref string) (d ocispec.Descriptor, err error) { diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index 72b0beae8..44a80b63a 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/cmd/ctr/commands/run" "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/log" "github.com/containerd/typeurl" "github.com/urfave/cli" @@ -364,15 +365,39 @@ var restoreCommand = cli.Command{ } defer cancel() + checkpoint, err := client.GetImage(ctx, ref) + if err != nil { + if !errdefs.IsNotFound(err) { + return err + } + ck, err := client.Fetch(ctx, ref) + if err != nil { + return err + } + checkpoint = containerd.NewImage(client, ck) + } + opts := []containerd.RestoreOpts{ containerd.WithRestoreSpec, containerd.WithRestoreSnapshot, containerd.WithRestoreRuntime, } - if context.Bool("live") { - opts = append(opts, containerd.WithRestoreLive) + + ctr, err := client.Restore(ctx, id, ref, opts...) + if err != nil { + return err } - if err := client.Restore(ctx, id, ref, opts...); err != nil { + + topts := []containerd.NewTaskOpts{} + if context.Bool("live") { + topts = append(topts, containerd.WithTaskCheckpoint(checkpoint)) + } + task, err := ctr.NewTask(ctx, cio.NewCreator(cio.WithStdio), topts...) + if err != nil { + return err + } + + if err := task.Start(ctx); err != nil { return err } diff --git a/container_restore_opts.go b/container_restore_opts.go index 3b23bc57f..27c1f7ada 100644 --- a/container_restore_opts.go +++ b/container_restore_opts.go @@ -39,26 +39,19 @@ var ( ) // RestoreOpts are options to manage the restore operation -type RestoreOpts func(context.Context, string, *Client, Image, *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) - -// WithRestoreLive restores the runtime and memory data for the container -func WithRestoreLive(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { - return nil, []NewTaskOpts{ - WithTaskCheckpoint(checkpoint), - }, nil -} +type RestoreOpts func(context.Context, string, *Client, Image, *imagespec.Index) ([]NewContainerOpts, error) // WithRestoreRuntime restores the runtime for the container -func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { +func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { runtimeName, ok := index.Annotations["runtime.name"] if !ok { - return nil, nil, ErrCheckpointIndexRuntimeNameNotFound + return nil, ErrCheckpointIndexRuntimeNameNotFound } // restore options if present m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeOptions) if err != nil { if err != ErrMediaTypeNotFound { - return nil, nil, err + return nil, err } } var options *ptypes.Any @@ -66,64 +59,64 @@ func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoi store := client.ContentStore() data, err := content.ReadBlob(ctx, store, *m) if err != nil { - return nil, nil, errors.Wrap(err, "unable to read checkpoint runtime") + return nil, errors.Wrap(err, "unable to read checkpoint runtime") } if err := proto.Unmarshal(data, options); err != nil { - return nil, nil, err + return nil, err } } return []NewContainerOpts{ WithRuntime(runtimeName, options), - }, nil, nil + }, nil } // WithRestoreSpec restores the spec from the checkpoint for the container -func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { +func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointConfig) if err != nil { - return nil, nil, err + return nil, err } store := client.ContentStore() data, err := content.ReadBlob(ctx, store, *m) if err != nil { - return nil, nil, errors.Wrap(err, "unable to read checkpoint config") + return nil, errors.Wrap(err, "unable to read checkpoint config") } var any ptypes.Any if err := proto.Unmarshal(data, &any); err != nil { - return nil, nil, err + return nil, err } v, err := typeurl.UnmarshalAny(&any) if err != nil { - return nil, nil, err + return nil, err } spec := v.(*oci.Spec) return []NewContainerOpts{ WithSpec(spec), - }, nil, nil + }, nil } -func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, []NewTaskOpts, error) { +// WithRestoreSnapshot restores the snapshot from the checkpoint for the container +func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { // get image from annotation imageName, ok := index.Annotations["image.name"] if !ok { - return nil, nil, ErrCheckpointIndexImageNameNotFound + return nil, ErrCheckpointIndexImageNameNotFound } i, err := client.Pull(ctx, imageName, WithPullUnpack) if err != nil { - return nil, nil, err + return nil, err } diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) if err != nil { - return nil, nil, err + return nil, err } - //setSnapshotterIfEmpty(client) parent := identity.ChainID(diffIDs).String() if _, err := client.SnapshotService(DefaultSnapshotter).Prepare(ctx, id, parent); err != nil { - return nil, nil, err + return nil, err } return []NewContainerOpts{ WithImage(i), WithSnapshot(id), - }, nil, nil + }, nil } From ce0673fd7df99aab48a68df19ae3feed50892ee3 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 17 Sep 2018 11:10:41 -0400 Subject: [PATCH 4/9] Restore take image Signed-off-by: Evan Hazlett --- client.go | 14 +------------- cmd/ctr/commands/containers/containers.go | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/client.go b/client.go index 01c1c2441..1566ef110 100644 --- a/client.go +++ b/client.go @@ -523,19 +523,7 @@ func (c *Client) ListImages(ctx context.Context, filters ...string) ([]Image, er } // Restore restores a container from a checkpoint -func (c *Client) Restore(ctx context.Context, id, ref string, opts ...RestoreOpts) (Container, error) { - checkpoint, err := c.GetImage(ctx, ref) - if err != nil { - if !errdefs.IsNotFound(err) { - return nil, err - } - ck, err := c.Fetch(ctx, ref) - if err != nil { - return nil, err - } - checkpoint = NewImage(c, ck) - } - +func (c *Client) Restore(ctx context.Context, id string, checkpoint Image, opts ...RestoreOpts) (Container, error) { store := c.ContentStore() index, err := decodeIndex(ctx, store, checkpoint.Target()) if err != nil { diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index 44a80b63a..a0420a44f 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -383,7 +383,7 @@ var restoreCommand = cli.Command{ containerd.WithRestoreRuntime, } - ctr, err := client.Restore(ctx, id, ref, opts...) + ctr, err := client.Restore(ctx, id, checkpoint, opts...) if err != nil { return err } From 147208061cbcf123db82050d13510c5009825bd3 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 17 Sep 2018 13:48:46 -0400 Subject: [PATCH 5/9] add image name and runtime name media types; remove task operation on checkpoint Signed-off-by: Evan Hazlett --- cmd/ctr/commands/containers/containers.go | 12 +++ container.go | 97 ++++++++++++----------- container_restore_opts.go | 75 ++++++++++++++---- images/mediatypes.go | 2 + 4 files changed, 123 insertions(+), 63 deletions(-) diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index a0420a44f..c21381947 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -332,6 +332,17 @@ var checkpointCommand = cli.Command{ if err != nil { return err } + task, err := container.Task(ctx, nil) + if err != nil { + if !errdefs.IsNotFound(err) { + return err + } + } + if err := task.Pause(ctx); err != nil { + return err + } + defer task.Resume(ctx) + if _, err := container.Checkpoint(ctx, ref, opts...); err != nil { return err } @@ -378,6 +389,7 @@ var restoreCommand = cli.Command{ } opts := []containerd.RestoreOpts{ + containerd.WithRestoreImage, containerd.WithRestoreSpec, containerd.WithRestoreSnapshot, containerd.WithRestoreRuntime, diff --git a/container.go b/container.go index 4d24adfc3..c20f526be 100644 --- a/container.go +++ b/container.go @@ -295,11 +295,15 @@ func (c *container) Checkpoint(ctx context.Context, ref string, opts ...Checkpoi FileLocks: true, EmptyNamespaces: nil, } + info, err := c.Info(ctx) + if err != nil { + return nil, err + } + img, err := c.Image(ctx) if err != nil { return nil, err } - index.Annotations["image.name"] = img.Name() ctx, done, err := c.client.WithLease(ctx) if err != nil { @@ -307,54 +311,53 @@ func (c *container) Checkpoint(ctx context.Context, ref string, opts ...Checkpoi } defer done(ctx) - // pause task to checkpoint - if err := func(ctx context.Context) error { - task, err := c.Task(ctx, nil) - if err != nil { - if errdefs.IsNotFound(err) { - return nil - } - return err - } - if err := task.Pause(ctx); err != nil { - return err - } - defer task.Resume(ctx) - - info, err := c.Info(ctx) - if err != nil { - return err - } - - // add runtime info to index - index.Annotations["runtime.name"] = info.Runtime.Name - if info.Runtime.Options != nil { - data, err := info.Runtime.Options.Marshal() - if err != nil { - return err - } - r := bytes.NewReader(data) - desc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeOptions, info.ID+"-runtime-options", r) - if err != nil { - return err - } - desc.Platform = &ocispec.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - } - index.Manifests = append(index.Manifests, desc) - } - - // process remaining opts - for _, o := range opts { - if err := o(ctx, c.client, &info, index, copts); err != nil { - return err - } - } - return nil - }(ctx); err != nil { + // add image name to manifest + ir := bytes.NewReader([]byte(img.Name())) + idesc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointImageName, info.ID+"-image-name", ir) + if err != nil { return nil, err } + idesc.Platform = &ocispec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + index.Manifests = append(index.Manifests, idesc) + + // add runtime info to index + rr := bytes.NewReader([]byte(info.Runtime.Name)) + rdesc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeName, info.ID+"-runtime-name", rr) + if err != nil { + return nil, err + } + rdesc.Platform = &ocispec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + index.Manifests = append(index.Manifests, rdesc) + + if info.Runtime.Options != nil { + data, err := info.Runtime.Options.Marshal() + if err != nil { + return nil, err + } + r := bytes.NewReader(data) + desc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeOptions, info.ID+"-runtime-options", r) + if err != nil { + return nil, err + } + desc.Platform = &ocispec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + index.Manifests = append(index.Manifests, desc) + } + + // process remaining opts + for _, o := range opts { + if err := o(ctx, c.client, &info, index, copts); err != nil { + return nil, err + } + } desc, err := writeIndex(ctx, index, c.client, c.ID()+"index") if err != nil { diff --git a/container_restore_opts.go b/container_restore_opts.go index 27c1f7ada..eeb353c34 100644 --- a/container_restore_opts.go +++ b/container_restore_opts.go @@ -31,22 +31,54 @@ import ( "github.com/pkg/errors" ) -var ( - // ErrCheckpointIndexImageNameNotFound is returned when the checkpoint image name is not present in the index - ErrCheckpointIndexImageNameNotFound = errors.New("image name not present in index") - // ErrCheckpointIndexRuntimeNameNotFound is returned when the checkpoint runtime name is not present in the index - ErrCheckpointIndexRuntimeNameNotFound = errors.New("runtime name not present in index") -) - // RestoreOpts are options to manage the restore operation type RestoreOpts func(context.Context, string, *Client, Image, *imagespec.Index) ([]NewContainerOpts, error) +// WithRestoreImage restores the image for the container +func WithRestoreImage(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { + store := client.ContentStore() + m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointImageName) + if err != nil { + if err != ErrMediaTypeNotFound { + return nil, err + } + } + imageName := "" + if m != nil { + data, err := content.ReadBlob(ctx, store, *m) + if err != nil { + return nil, err + } + imageName = string(data) + } + i, err := client.GetImage(ctx, imageName) + if err != nil { + return nil, err + } + + return []NewContainerOpts{ + WithImage(i), + }, nil +} + // WithRestoreRuntime restores the runtime for the container func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { - runtimeName, ok := index.Annotations["runtime.name"] - if !ok { - return nil, ErrCheckpointIndexRuntimeNameNotFound + store := client.ContentStore() + n, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeName) + if err != nil { + if err != ErrMediaTypeNotFound { + return nil, err + } } + runtimeName := "" + if n != nil { + data, err := content.ReadBlob(ctx, store, *n) + if err != nil { + return nil, err + } + runtimeName = string(data) + } + // restore options if present m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeOptions) if err != nil { @@ -54,9 +86,9 @@ func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoi return nil, err } } + var options *ptypes.Any if m != nil { - store := client.ContentStore() data, err := content.ReadBlob(ctx, store, *m) if err != nil { return nil, errors.Wrap(err, "unable to read checkpoint runtime") @@ -98,15 +130,26 @@ func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint // WithRestoreSnapshot restores the snapshot from the checkpoint for the container func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { - // get image from annotation - imageName, ok := index.Annotations["image.name"] - if !ok { - return nil, ErrCheckpointIndexImageNameNotFound + imageName := "" + store := client.ContentStore() + m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointImageName) + if err != nil { + if err != ErrMediaTypeNotFound { + return nil, err + } } - i, err := client.Pull(ctx, imageName, WithPullUnpack) + if m != nil { + data, err := content.ReadBlob(ctx, store, *m) + if err != nil { + return nil, err + } + imageName = string(data) + } + i, err := client.GetImage(ctx, imageName) if err != nil { return nil, err } + diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) if err != nil { return nil, err diff --git a/images/mediatypes.go b/images/mediatypes.go index e7a84526f..005a2720b 100644 --- a/images/mediatypes.go +++ b/images/mediatypes.go @@ -34,6 +34,8 @@ const ( MediaTypeContainerd1Resource = "application/vnd.containerd.container.resource.tar" MediaTypeContainerd1RW = "application/vnd.containerd.container.rw.tar" MediaTypeContainerd1CheckpointConfig = "application/vnd.containerd.container.checkpoint.config.v1+proto" + MediaTypeContainerd1CheckpointImageName = "application/vnd.containerd.container.checkpoint.image.name" + MediaTypeContainerd1CheckpointRuntimeName = "application/vnd.containerd.container.checkpoint.runtime.name" MediaTypeContainerd1CheckpointRuntimeOptions = "application/vnd.containerd.container.checkpoint.runtime.options+proto" // Legacy Docker schema1 manifest MediaTypeDockerSchema1Manifest = "application/vnd.docker.distribution.manifest.v1+prettyjws" From 4fdf720b843a9d215a0978382b6e045f503d445d Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Mon, 17 Sep 2018 18:22:36 -0400 Subject: [PATCH 6/9] move rw to opt; make snapshot opt; move to NewContainerOpts Signed-off-by: Evan Hazlett --- client.go | 26 +-- cmd/ctr/commands/containers/containers.go | 22 ++- container.go | 47 +---- container_checkpoint_opts.go | 38 ++-- container_restore_opts.go | 206 +++++++++++----------- 5 files changed, 152 insertions(+), 187 deletions(-) diff --git a/client.go b/client.go index 1566ef110..bc86590c2 100644 --- a/client.go +++ b/client.go @@ -538,11 +538,7 @@ func (c *Client) Restore(ctx context.Context, id string, checkpoint Image, opts copts := []NewContainerOpts{} for _, o := range opts { - co, err := o(ctx, id, c, checkpoint, index) - if err != nil { - return nil, err - } - copts = append(copts, co...) + copts = append(copts, o(ctx, id, c, checkpoint, index)) } ctr, err := c.NewContainer(ctx, id, copts...) @@ -550,26 +546,6 @@ func (c *Client) Restore(ctx context.Context, id string, checkpoint Image, opts return nil, err } - // apply rw layer - info, err := ctr.Info(ctx) - if err != nil { - return nil, err - } - - rw, err := GetIndexByMediaType(index, ocispec.MediaTypeImageLayerGzip) - if err != nil { - return nil, err - } - - mounts, err := c.SnapshotService(info.Snapshotter).Mounts(ctx, info.SnapshotKey) - if err != nil { - return nil, err - } - - if _, err := c.DiffService().Apply(ctx, *rw, mounts); err != nil { - return nil, err - } - return ctr, nil } diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index c21381947..af5cbd539 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -318,7 +318,10 @@ var checkpointCommand = cli.Command{ return err } defer cancel() - opts := []containerd.CheckpointOpts{} + opts := []containerd.CheckpointOpts{ + containerd.WithCheckpointRuntime, + } + if context.Bool("image") { opts = append(opts, containerd.WithCheckpointImage) } @@ -338,10 +341,13 @@ var checkpointCommand = cli.Command{ return err } } - if err := task.Pause(ctx); err != nil { - return err + // pause if running + if task != nil { + if err := task.Pause(ctx); err != nil { + return err + } + defer task.Resume(ctx) } - defer task.Resume(ctx) if _, err := container.Checkpoint(ctx, ref, opts...); err != nil { return err @@ -360,6 +366,10 @@ var restoreCommand = cli.Command{ Name: "live", Usage: "restore the runtime and memory data from the checkpoint", }, + cli.BoolFlag{ + Name: "rw", + Usage: "restore the rw layer from the checkpoint", + }, }, Action: func(context *cli.Context) error { id := context.Args().First() @@ -394,6 +404,9 @@ var restoreCommand = cli.Command{ containerd.WithRestoreSnapshot, containerd.WithRestoreRuntime, } + if context.Bool("rw") { + opts = append(opts, containerd.WithRestoreRW) + } ctr, err := client.Restore(ctx, id, checkpoint, opts...) if err != nil { @@ -404,6 +417,7 @@ var restoreCommand = cli.Command{ if context.Bool("live") { topts = append(topts, containerd.WithTaskCheckpoint(checkpoint)) } + task, err := ctr.NewTask(ctx, cio.NewCreator(cio.WithStdio), topts...) if err != nil { return err diff --git a/container.go b/container.go index c20f526be..39993faae 100644 --- a/container.go +++ b/container.go @@ -17,12 +17,10 @@ package containerd import ( - "bytes" "context" "encoding/json" "os" "path/filepath" - "runtime" "strings" "github.com/containerd/containerd/api/services/tasks/v1" @@ -40,6 +38,11 @@ import ( "github.com/pkg/errors" ) +const ( + checkpointImageNameLabel = "image.name" + checkpointRuntimeNameLabel = "runtime.name" +) + // Container is a metadata object for container resources and task creation type Container interface { // ID identifies the container @@ -312,45 +315,9 @@ func (c *container) Checkpoint(ctx context.Context, ref string, opts ...Checkpoi defer done(ctx) // add image name to manifest - ir := bytes.NewReader([]byte(img.Name())) - idesc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointImageName, info.ID+"-image-name", ir) - if err != nil { - return nil, err - } - idesc.Platform = &ocispec.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - } - index.Manifests = append(index.Manifests, idesc) - + index.Annotations[checkpointImageNameLabel] = img.Name() // add runtime info to index - rr := bytes.NewReader([]byte(info.Runtime.Name)) - rdesc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeName, info.ID+"-runtime-name", rr) - if err != nil { - return nil, err - } - rdesc.Platform = &ocispec.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - } - index.Manifests = append(index.Manifests, rdesc) - - if info.Runtime.Options != nil { - data, err := info.Runtime.Options.Marshal() - if err != nil { - return nil, err - } - r := bytes.NewReader(data) - desc, err := writeContent(ctx, c.client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeOptions, info.ID+"-runtime-options", r) - if err != nil { - return nil, err - } - desc.Platform = &ocispec.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - } - index.Manifests = append(index.Manifests, desc) - } + index.Annotations[checkpointRuntimeNameLabel] = info.Runtime.Name // process remaining opts for _, o := range opts { diff --git a/container_checkpoint_opts.go b/container_checkpoint_opts.go index b0d618070..3f78a4f12 100644 --- a/container_checkpoint_opts.go +++ b/container_checkpoint_opts.go @@ -17,6 +17,7 @@ package containerd import ( + "bytes" "context" "fmt" "runtime" @@ -24,6 +25,7 @@ import ( tasks "github.com/containerd/containerd/api/services/tasks/v1" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/diff" + "github.com/containerd/containerd/images" "github.com/containerd/containerd/rootfs" "github.com/containerd/containerd/runtime/v2/runc/options" "github.com/containerd/typeurl" @@ -78,23 +80,35 @@ func WithCheckpointTask(ctx context.Context, client *Client, c *containers.Conta return nil } +// WithCheckpointRuntime includes the container runtime info +func WithCheckpointRuntime(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error { + if c.Runtime.Options != nil { + data, err := c.Runtime.Options.Marshal() + if err != nil { + return err + } + r := bytes.NewReader(data) + desc, err := writeContent(ctx, client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeOptions, c.ID+"-runtime-options", r) + if err != nil { + return err + } + desc.Platform = &imagespec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + index.Manifests = append(index.Manifests, desc) + } + return nil +} + // WithCheckpointRW includes the rw in the checkpoint func WithCheckpointRW(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error { - cnt, err := client.LoadContainer(ctx, c.ID) - if err != nil { - return err - } - info, err := cnt.Info(ctx) - if err != nil { - return err - } - diffOpts := []diff.Opt{ - diff.WithReference(fmt.Sprintf("checkpoint-rw-%s", info.SnapshotKey)), + diff.WithReference(fmt.Sprintf("checkpoint-rw-%s", c.SnapshotKey)), } rw, err := rootfs.CreateDiff(ctx, - info.SnapshotKey, - client.SnapshotService(info.Snapshotter), + c.SnapshotKey, + client.SnapshotService(c.Snapshotter), client.DiffService(), diffOpts..., ) diff --git a/container_restore_opts.go b/container_restore_opts.go index eeb353c34..a6b41ff9d 100644 --- a/container_restore_opts.go +++ b/container_restore_opts.go @@ -19,11 +19,10 @@ package containerd import ( "context" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" - "github.com/containerd/containerd/oci" "github.com/containerd/containerd/platforms" - "github.com/containerd/typeurl" "github.com/gogo/protobuf/proto" ptypes "github.com/gogo/protobuf/types" "github.com/opencontainers/image-spec/identity" @@ -31,135 +30,130 @@ import ( "github.com/pkg/errors" ) +var ( + // ErrImageNameNotFoundInIndex is returned when the image name is not found in the index + ErrImageNameNotFoundInIndex = errors.New("image name not found in index") + // ErrRuntimeNameNotFoundInIndex is returned when the runtime name is not found in the index + ErrRuntimeNameNotFoundInIndex = errors.New("runtime name not found in index") +) + // RestoreOpts are options to manage the restore operation -type RestoreOpts func(context.Context, string, *Client, Image, *imagespec.Index) ([]NewContainerOpts, error) +type RestoreOpts func(context.Context, string, *Client, Image, *imagespec.Index) NewContainerOpts // WithRestoreImage restores the image for the container -func WithRestoreImage(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { - store := client.ContentStore() - m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointImageName) - if err != nil { - if err != ErrMediaTypeNotFound { - return nil, err +func WithRestoreImage(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + name, ok := index.Annotations[checkpointImageNameLabel] + if !ok || name == "" { + return ErrRuntimeNameNotFoundInIndex } - } - imageName := "" - if m != nil { - data, err := content.ReadBlob(ctx, store, *m) + i, err := client.GetImage(ctx, name) if err != nil { - return nil, err + return err } - imageName = string(data) - } - i, err := client.GetImage(ctx, imageName) - if err != nil { - return nil, err - } - return []NewContainerOpts{ - WithImage(i), - }, nil + c.Image = i.Name() + return nil + } } // WithRestoreRuntime restores the runtime for the container -func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { - store := client.ContentStore() - n, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeName) - if err != nil { - if err != ErrMediaTypeNotFound { - return nil, err +func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + name, ok := index.Annotations[checkpointRuntimeNameLabel] + if !ok { + return ErrRuntimeNameNotFoundInIndex } - } - runtimeName := "" - if n != nil { - data, err := content.ReadBlob(ctx, store, *n) - if err != nil { - return nil, err - } - runtimeName = string(data) - } - // restore options if present - m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeOptions) - if err != nil { - if err != ErrMediaTypeNotFound { - return nil, err - } - } - - var options *ptypes.Any - if m != nil { - data, err := content.ReadBlob(ctx, store, *m) + // restore options if present + m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeOptions) if err != nil { - return nil, errors.Wrap(err, "unable to read checkpoint runtime") + if err != ErrMediaTypeNotFound { + return err + } } - if err := proto.Unmarshal(data, options); err != nil { - return nil, err + var options *ptypes.Any + if m != nil { + store := client.ContentStore() + data, err := content.ReadBlob(ctx, store, *m) + if err != nil { + return errors.Wrap(err, "unable to read checkpoint runtime") + } + if err := proto.Unmarshal(data, options); err != nil { + return err + } } + + c.Runtime = containers.RuntimeInfo{ + Name: name, + Options: options, + } + return nil } - return []NewContainerOpts{ - WithRuntime(runtimeName, options), - }, nil } // WithRestoreSpec restores the spec from the checkpoint for the container -func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { - m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointConfig) - if err != nil { - return nil, err +func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointConfig) + if err != nil { + return err + } + store := client.ContentStore() + data, err := content.ReadBlob(ctx, store, *m) + if err != nil { + return errors.Wrap(err, "unable to read checkpoint config") + } + var any ptypes.Any + if err := proto.Unmarshal(data, &any); err != nil { + return err + } + c.Spec = &any + return nil } - store := client.ContentStore() - data, err := content.ReadBlob(ctx, store, *m) - if err != nil { - return nil, errors.Wrap(err, "unable to read checkpoint config") - } - var any ptypes.Any - if err := proto.Unmarshal(data, &any); err != nil { - return nil, err - } - - v, err := typeurl.UnmarshalAny(&any) - if err != nil { - return nil, err - } - spec := v.(*oci.Spec) - return []NewContainerOpts{ - WithSpec(spec), - }, nil } // WithRestoreSnapshot restores the snapshot from the checkpoint for the container -func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) ([]NewContainerOpts, error) { - imageName := "" - store := client.ContentStore() - m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointImageName) - if err != nil { - if err != ErrMediaTypeNotFound { - return nil, err +func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + imageName, ok := index.Annotations[checkpointImageNameLabel] + if !ok || imageName == "" { + return ErrRuntimeNameNotFoundInIndex } - } - if m != nil { - data, err := content.ReadBlob(ctx, store, *m) + i, err := client.GetImage(ctx, imageName) if err != nil { - return nil, err + return err } - imageName = string(data) + diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) + if err != nil { + return err + } + parent := identity.ChainID(diffIDs).String() + if _, err := client.SnapshotService(DefaultSnapshotter).Prepare(ctx, id, parent); err != nil { + return err + } + c.SnapshotKey = id + c.Snapshotter = DefaultSnapshotter + return nil + } +} + +// WithRestoreSnapshot restores the snapshot from the checkpoint for the container +func WithRestoreRW(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + // apply rw layer + rw, err := GetIndexByMediaType(index, imagespec.MediaTypeImageLayerGzip) + if err != nil { + return err + } + mounts, err := client.SnapshotService(c.Snapshotter).Mounts(ctx, c.SnapshotKey) + if err != nil { + return err + } + + if _, err := client.DiffService().Apply(ctx, *rw, mounts); err != nil { + return err + } + return nil } - i, err := client.GetImage(ctx, imageName) - if err != nil { - return nil, err - } - - diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) - if err != nil { - return nil, err - } - parent := identity.ChainID(diffIDs).String() - if _, err := client.SnapshotService(DefaultSnapshotter).Prepare(ctx, id, parent); err != nil { - return nil, err - } - return []NewContainerOpts{ - WithImage(i), - WithSnapshot(id), - }, nil } From 40caece8dc9426eef146c24b8c03d77c78af832c Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 20 Sep 2018 21:52:47 -0400 Subject: [PATCH 7/9] update tests Signed-off-by: Evan Hazlett --- cmd/ctr/commands/commands.go | 4 -- cmd/ctr/commands/run/run_unix.go | 8 --- container_checkpoint_opts.go | 6 ++ container_checkpoint_test.go | 104 +++++++++++++++++++++++-------- container_opts_unix.go | 69 -------------------- container_restore_opts.go | 2 +- 6 files changed, 84 insertions(+), 109 deletions(-) diff --git a/cmd/ctr/commands/commands.go b/cmd/ctr/commands/commands.go index cba982454..7c3d95998 100644 --- a/cmd/ctr/commands/commands.go +++ b/cmd/ctr/commands/commands.go @@ -71,10 +71,6 @@ var ( Name: "config,c", Usage: "path to the runtime-specific spec config file", }, - cli.StringFlag{ - Name: "checkpoint", - Usage: "provide the checkpoint digest to restore the container", - }, cli.StringFlag{ Name: "cwd", Usage: "specify the working directory of the process", diff --git a/cmd/ctr/commands/run/run_unix.go b/cmd/ctr/commands/run/run_unix.go index 11c5f2f50..3a0f813c5 100644 --- a/cmd/ctr/commands/run/run_unix.go +++ b/cmd/ctr/commands/run/run_unix.go @@ -44,14 +44,6 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli id = context.Args().Get(1) } - if raw := context.String("checkpoint"); raw != "" { - im, err := client.GetImage(ctx, raw) - if err != nil { - return nil, err - } - return client.NewContainer(ctx, id, containerd.WithCheckpoint(im, id), containerd.WithRuntime(context.String("runtime"), nil)) - } - var ( opts []oci.SpecOpts cOpts []containerd.NewContainerOpts diff --git a/container_checkpoint_opts.go b/container_checkpoint_opts.go index 3f78a4f12..f336fb219 100644 --- a/container_checkpoint_opts.go +++ b/container_checkpoint_opts.go @@ -124,6 +124,12 @@ func WithCheckpointRW(ctx context.Context, client *Client, c *containers.Contain return nil } +// WithCheckpointTaskExit causes the task to exit after checkpoint +func WithCheckpointTaskExit(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error { + copts.Exit = true + return nil +} + // GetIndexByMediaType returns the index in a manifest for the specified media type func GetIndexByMediaType(index *imagespec.Index, mt string) (*imagespec.Descriptor, error) { for _, d := range index.Manifests { diff --git a/container_checkpoint_test.go b/container_checkpoint_test.go index 695cfdac8..c4f0d7f37 100644 --- a/container_checkpoint_test.go +++ b/container_checkpoint_test.go @@ -28,8 +28,11 @@ import ( "testing" "github.com/containerd/containerd/oci" - "github.com/containerd/containerd/runtime/linux/runctypes" - "github.com/containerd/containerd/runtime/v2/runc/options" +) + +const ( + v1runtime = "io.containerd.runtime.v1.linux" + testCheckpointName = "checkpoint-test:latest" ) func TestCheckpointRestorePTY(t *testing.T) { @@ -41,6 +44,9 @@ func TestCheckpointRestorePTY(t *testing.T) { t.Fatal(err) } defer client.Close() + if client.runtime == v1runtime { + t.Skip() + } var ( ctx, cancel = testContext() @@ -56,7 +62,8 @@ func TestCheckpointRestorePTY(t *testing.T) { WithNewSnapshot(id, image), WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sh", "-c", "read A; echo z${A}z"), - oci.WithTTY)) + oci.WithTTY), + ) if err != nil { t.Fatal(err) } @@ -83,7 +90,12 @@ func TestCheckpointRestorePTY(t *testing.T) { t.Fatal(err) } - checkpoint, err := task.Checkpoint(ctx, withExit(client)) + checkpoint, err := container.Checkpoint(ctx, testCheckpointName+"withpty", []CheckpointOpts{ + WithCheckpointRuntime, + WithCheckpointRW, + WithCheckpointTaskExit, + WithCheckpointTask, + }...) if err != nil { t.Fatal(err) } @@ -94,6 +106,10 @@ func TestCheckpointRestorePTY(t *testing.T) { t.Fatal(err) } direct.Delete() + if err := container.Delete(ctx, WithSnapshotCleanup); err != nil { + t.Fatal(err) + } + direct, err = newDirectIO(ctx, true) if err != nil { t.Fatal(err) @@ -109,6 +125,15 @@ func TestCheckpointRestorePTY(t *testing.T) { io.Copy(buf, direct.Stdout) }() + if container, err = client.Restore(ctx, id, checkpoint, []RestoreOpts{ + WithRestoreImage, + WithRestoreSpec, + WithRestoreSnapshot, + WithRestoreRuntime, + WithRestoreRW, + }...); err != nil { + t.Fatal(err) + } if task, err = container.NewTask(ctx, direct.IOCreate, WithTaskCheckpoint(checkpoint)); err != nil { t.Fatal(err) @@ -146,6 +171,9 @@ func TestCheckpointRestore(t *testing.T) { t.Fatal(err) } defer client.Close() + if client.runtime == v1runtime { + t.Skip() + } var ( ctx, cancel = testContext() @@ -157,7 +185,7 @@ func TestCheckpointRestore(t *testing.T) { if err != nil { t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSnapshot(id, image), WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sleep", "100"))) + container, err := client.NewContainer(ctx, id, WithNewSnapshot(id, image), WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sleep", "10"))) if err != nil { t.Fatal(err) } @@ -178,7 +206,11 @@ func TestCheckpointRestore(t *testing.T) { t.Fatal(err) } - checkpoint, err := task.Checkpoint(ctx, withExit(client)) + checkpoint, err := container.Checkpoint(ctx, testCheckpointName+"restore", []CheckpointOpts{ + WithCheckpointRuntime, + WithCheckpointRW, + WithCheckpointTask, + }...) if err != nil { t.Fatal(err) } @@ -188,6 +220,19 @@ func TestCheckpointRestore(t *testing.T) { if _, err := task.Delete(ctx); err != nil { t.Fatal(err) } + if err := container.Delete(ctx, WithSnapshotCleanup); err != nil { + t.Fatal(err) + } + + if container, err = client.Restore(ctx, id, checkpoint, []RestoreOpts{ + WithRestoreImage, + WithRestoreSpec, + WithRestoreSnapshot, + WithRestoreRuntime, + WithRestoreRW, + }...); err != nil { + t.Fatal(err) + } if task, err = container.NewTask(ctx, empty(), WithTaskCheckpoint(checkpoint)); err != nil { t.Fatal(err) } @@ -217,6 +262,9 @@ func TestCheckpointRestoreNewContainer(t *testing.T) { t.Fatal(err) } defer client.Close() + if client.runtime == v1runtime { + t.Skip() + } id := t.Name() ctx, cancel := testContext() @@ -226,7 +274,7 @@ func TestCheckpointRestoreNewContainer(t *testing.T) { if err != nil { t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSnapshot(id, image), WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sleep", "100"))) + container, err := client.NewContainer(ctx, id, WithNewSnapshot(id, image), WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sleep", "5"))) if err != nil { t.Fatal(err) } @@ -247,7 +295,11 @@ func TestCheckpointRestoreNewContainer(t *testing.T) { t.Fatal(err) } - checkpoint, err := task.Checkpoint(ctx, withExit(client)) + checkpoint, err := container.Checkpoint(ctx, testCheckpointName+"newcontainer", []CheckpointOpts{ + WithCheckpointRuntime, + WithCheckpointRW, + WithCheckpointTask, + }...) if err != nil { t.Fatal(err) } @@ -260,7 +312,13 @@ func TestCheckpointRestoreNewContainer(t *testing.T) { if err := container.Delete(ctx, WithSnapshotCleanup); err != nil { t.Fatal(err) } - if container, err = client.NewContainer(ctx, id, WithCheckpoint(checkpoint, id)); err != nil { + if container, err = client.Restore(ctx, id, checkpoint, []RestoreOpts{ + WithRestoreImage, + WithRestoreSpec, + WithRestoreSnapshot, + WithRestoreRuntime, + WithRestoreRW, + }...); err != nil { t.Fatal(err) } if task, err = container.NewTask(ctx, empty(), WithTaskCheckpoint(checkpoint)); err != nil { @@ -290,11 +348,14 @@ func TestCheckpointLeaveRunning(t *testing.T) { if !supportsCriu { t.Skip("system does not have criu installed") } - client, err := New(address) + client, err := newClient(t, address) if err != nil { t.Fatal(err) } defer client.Close() + if client.runtime == v1runtime { + t.Skip() + } var ( ctx, cancel = testContext() @@ -327,7 +388,12 @@ func TestCheckpointLeaveRunning(t *testing.T) { t.Fatal(err) } - if _, err := task.Checkpoint(ctx); err != nil { + // checkpoint + if _, err := container.Checkpoint(ctx, testCheckpointName+"leaverunning", []CheckpointOpts{ + WithCheckpointRuntime, + WithCheckpointRW, + WithCheckpointTask, + }...); err != nil { t.Fatal(err) } @@ -345,19 +411,3 @@ func TestCheckpointLeaveRunning(t *testing.T) { <-statusC } - -func withExit(client *Client) CheckpointTaskOpts { - return func(r *CheckpointTaskInfo) error { - switch client.runtime { - case "io.containerd.runc.v1": - r.Options = &options.CheckpointOptions{ - Exit: true, - } - default: - r.Options = &runctypes.CheckpointOptions{ - Exit: true, - } - } - return nil - } -} diff --git a/container_opts_unix.go b/container_opts_unix.go index c0622f67f..9e013f1a4 100644 --- a/container_opts_unix.go +++ b/container_opts_unix.go @@ -26,81 +26,12 @@ import ( "syscall" "github.com/containerd/containerd/containers" - "github.com/containerd/containerd/content" "github.com/containerd/containerd/errdefs" - "github.com/containerd/containerd/images" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/platforms" - "github.com/gogo/protobuf/proto" - protobuf "github.com/gogo/protobuf/types" "github.com/opencontainers/image-spec/identity" - "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" ) -// WithCheckpoint allows a container to be created from the checkpointed information -// provided by the descriptor. The image, snapshot, and runtime specifications are -// restored on the container -func WithCheckpoint(im Image, snapshotKey string) NewContainerOpts { - // set image and rw, and spec - return func(ctx context.Context, client *Client, c *containers.Container) error { - var ( - desc = im.Target() - store = client.ContentStore() - ) - index, err := decodeIndex(ctx, store, desc) - if err != nil { - return err - } - var rw *v1.Descriptor - for _, m := range index.Manifests { - switch m.MediaType { - case v1.MediaTypeImageLayer: - fk := m - rw = &fk - case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList: - config, err := images.Config(ctx, store, m, platforms.Default()) - if err != nil { - return errors.Wrap(err, "unable to resolve image config") - } - diffIDs, err := images.RootFS(ctx, store, config) - if err != nil { - return errors.Wrap(err, "unable to get rootfs") - } - setSnapshotterIfEmpty(c) - if _, err := client.SnapshotService(c.Snapshotter).Prepare(ctx, snapshotKey, identity.ChainID(diffIDs).String()); err != nil { - if !errdefs.IsAlreadyExists(err) { - return err - } - } - c.Image = index.Annotations["image.name"] - case images.MediaTypeContainerd1CheckpointConfig: - data, err := content.ReadBlob(ctx, store, m) - if err != nil { - return errors.Wrap(err, "unable to read checkpoint config") - } - var any protobuf.Any - if err := proto.Unmarshal(data, &any); err != nil { - return err - } - c.Spec = &any - } - } - if rw != nil { - // apply the rw snapshot to the new rw layer - mounts, err := client.SnapshotService(c.Snapshotter).Mounts(ctx, snapshotKey) - if err != nil { - return errors.Wrapf(err, "unable to get mounts for %s", snapshotKey) - } - if _, err := client.DiffService().Apply(ctx, *rw, mounts); err != nil { - return errors.Wrap(err, "unable to apply rw diff") - } - } - c.SnapshotKey = snapshotKey - return nil - } -} - // WithRemappedSnapshot creates a new snapshot and remaps the uid/gid for the // filesystem to be used by a container with user namespaces func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts { diff --git a/container_restore_opts.go b/container_restore_opts.go index a6b41ff9d..bfb6effb0 100644 --- a/container_restore_opts.go +++ b/container_restore_opts.go @@ -138,7 +138,7 @@ func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpo } } -// WithRestoreSnapshot restores the snapshot from the checkpoint for the container +// WithRestoreRW restores the rw layer from the checkpoint for the container func WithRestoreRW(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { // apply rw layer From 6f2f4e4343baa64b5e907e7992e4ed451d1c7747 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 25 Sep 2018 15:05:37 +0000 Subject: [PATCH 8/9] checkpoint: add copts to checkpoint; save snapshotter to annotation Signed-off-by: Evan Hazlett --- cmd/ctr/commands/containers/containers.go | 22 ++++++----- container.go | 7 +++- container_checkpoint_opts.go | 22 +++++++++-- container_checkpoint_test.go | 3 -- container_restore_opts.go | 45 +++++++++-------------- images/mediatypes.go | 2 +- 6 files changed, 55 insertions(+), 46 deletions(-) diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index af5cbd539..9ff476ba1 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -18,7 +18,6 @@ package containers import ( "context" - "errors" "fmt" "os" "strings" @@ -32,6 +31,7 @@ import ( "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/log" "github.com/containerd/typeurl" + "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -309,7 +309,7 @@ var checkpointCommand = cli.Command{ if id == "" { return errors.New("container id must be provided") } - ref := context.Args()[1] + ref := context.Args().Get(1) if ref == "" { return errors.New("ref must be provided") } @@ -346,7 +346,11 @@ var checkpointCommand = cli.Command{ if err := task.Pause(ctx); err != nil { return err } - defer task.Resume(ctx) + defer func() { + if err := task.Resume(ctx); err != nil { + fmt.Println(errors.Wrap(err, "error resuming task")) + } + }() } if _, err := container.Checkpoint(ctx, ref, opts...); err != nil { @@ -362,21 +366,21 @@ var restoreCommand = cli.Command{ Usage: "restore a container from checkpoint", ArgsUsage: "CONTAINER REF", Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "live", - Usage: "restore the runtime and memory data from the checkpoint", - }, cli.BoolFlag{ Name: "rw", Usage: "restore the rw layer from the checkpoint", }, + cli.BoolFlag{ + Name: "live", + Usage: "restore the runtime and memory data from the checkpoint", + }, }, Action: func(context *cli.Context) error { id := context.Args().First() if id == "" { return errors.New("container id must be provided") } - ref := context.Args()[1] + ref := context.Args().Get(1) if ref == "" { return errors.New("ref must be provided") } @@ -391,6 +395,7 @@ var restoreCommand = cli.Command{ if !errdefs.IsNotFound(err) { return err } + // TODO (ehazlett): consider other options (always/never fetch) ck, err := client.Fetch(ctx, ref) if err != nil { return err @@ -401,7 +406,6 @@ var restoreCommand = cli.Command{ opts := []containerd.RestoreOpts{ containerd.WithRestoreImage, containerd.WithRestoreSpec, - containerd.WithRestoreSnapshot, containerd.WithRestoreRuntime, } if context.Bool("rw") { diff --git a/container.go b/container.go index 39993faae..26247592b 100644 --- a/container.go +++ b/container.go @@ -39,8 +39,9 @@ import ( ) const ( - checkpointImageNameLabel = "image.name" - checkpointRuntimeNameLabel = "runtime.name" + checkpointImageNameLabel = "org.opencontainers.image.ref.name" + checkpointRuntimeNameLabel = "io.containerd.checkpoint.runtime" + checkpointSnapshotterNameLabel = "io.containerd.checkpoint.snapshotter" ) // Container is a metadata object for container resources and task creation @@ -318,6 +319,8 @@ func (c *container) Checkpoint(ctx context.Context, ref string, opts ...Checkpoi index.Annotations[checkpointImageNameLabel] = img.Name() // add runtime info to index index.Annotations[checkpointRuntimeNameLabel] = info.Runtime.Name + // add snapshotter info to index + index.Annotations[checkpointSnapshotterNameLabel] = info.Snapshotter // process remaining opts for _, o := range opts { diff --git a/container_checkpoint_opts.go b/container_checkpoint_opts.go index f336fb219..7d261421e 100644 --- a/container_checkpoint_opts.go +++ b/container_checkpoint_opts.go @@ -26,6 +26,7 @@ import ( "github.com/containerd/containerd/containers" "github.com/containerd/containerd/diff" "github.com/containerd/containerd/images" + "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/rootfs" "github.com/containerd/containerd/runtime/v2/runc/options" "github.com/containerd/typeurl" @@ -67,16 +68,29 @@ func WithCheckpointTask(ctx context.Context, client *Client, c *containers.Conta return err } for _, d := range task.Descriptors { + platformSpec := platforms.DefaultSpec() index.Manifests = append(index.Manifests, imagespec.Descriptor{ MediaType: d.MediaType, Size: d.Size_, Digest: d.Digest, - Platform: &imagespec.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - }, + Platform: &platformSpec, }) } + // save copts + data, err := any.Marshal() + if err != nil { + return err + } + r := bytes.NewReader(data) + desc, err := writeContent(ctx, client.ContentStore(), images.MediaTypeContainerd1CheckpointOptions, c.ID+"-checkpoint-options", r) + if err != nil { + return err + } + desc.Platform = &imagespec.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + index.Manifests = append(index.Manifests, desc) return nil } diff --git a/container_checkpoint_test.go b/container_checkpoint_test.go index c4f0d7f37..a120ce4c4 100644 --- a/container_checkpoint_test.go +++ b/container_checkpoint_test.go @@ -128,7 +128,6 @@ func TestCheckpointRestorePTY(t *testing.T) { if container, err = client.Restore(ctx, id, checkpoint, []RestoreOpts{ WithRestoreImage, WithRestoreSpec, - WithRestoreSnapshot, WithRestoreRuntime, WithRestoreRW, }...); err != nil { @@ -227,7 +226,6 @@ func TestCheckpointRestore(t *testing.T) { if container, err = client.Restore(ctx, id, checkpoint, []RestoreOpts{ WithRestoreImage, WithRestoreSpec, - WithRestoreSnapshot, WithRestoreRuntime, WithRestoreRW, }...); err != nil { @@ -315,7 +313,6 @@ func TestCheckpointRestoreNewContainer(t *testing.T) { if container, err = client.Restore(ctx, id, checkpoint, []RestoreOpts{ WithRestoreImage, WithRestoreSpec, - WithRestoreSnapshot, WithRestoreRuntime, WithRestoreRW, }...); err != nil { diff --git a/container_restore_opts.go b/container_restore_opts.go index bfb6effb0..4f251c4a6 100644 --- a/container_restore_opts.go +++ b/container_restore_opts.go @@ -33,8 +33,10 @@ import ( var ( // ErrImageNameNotFoundInIndex is returned when the image name is not found in the index ErrImageNameNotFoundInIndex = errors.New("image name not found in index") - // ErrRuntimeNameNotFoundInIndex is returned when the runtime name is not found in the index - ErrRuntimeNameNotFoundInIndex = errors.New("runtime name not found in index") + // ErrRuntimeNameNotFoundInIndex is returned when the runtime is not found in the index + ErrRuntimeNameNotFoundInIndex = errors.New("runtime not found in index") + // ErrSnapshotterNameNotFoundInIndex is returned when the snapshotter is not found in the index + ErrSnapshotterNameNotFoundInIndex = errors.New("snapshotter not found in index") ) // RestoreOpts are options to manage the restore operation @@ -47,12 +49,26 @@ func WithRestoreImage(ctx context.Context, id string, client *Client, checkpoint if !ok || name == "" { return ErrRuntimeNameNotFoundInIndex } + snapshotter, ok := index.Annotations[checkpointSnapshotterNameLabel] + if !ok || name == "" { + return ErrSnapshotterNameNotFoundInIndex + } i, err := client.GetImage(ctx, name) if err != nil { return err } + diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) + if err != nil { + return err + } + parent := identity.ChainID(diffIDs).String() + if _, err := client.SnapshotService(snapshotter).Prepare(ctx, id, parent); err != nil { + return err + } c.Image = i.Name() + c.SnapshotKey = id + c.Snapshotter = snapshotter return nil } } @@ -113,31 +129,6 @@ func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint } } -// WithRestoreSnapshot restores the snapshot from the checkpoint for the container -func WithRestoreSnapshot(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { - return func(ctx context.Context, client *Client, c *containers.Container) error { - imageName, ok := index.Annotations[checkpointImageNameLabel] - if !ok || imageName == "" { - return ErrRuntimeNameNotFoundInIndex - } - i, err := client.GetImage(ctx, imageName) - if err != nil { - return err - } - diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) - if err != nil { - return err - } - parent := identity.ChainID(diffIDs).String() - if _, err := client.SnapshotService(DefaultSnapshotter).Prepare(ctx, id, parent); err != nil { - return err - } - c.SnapshotKey = id - c.Snapshotter = DefaultSnapshotter - return nil - } -} - // WithRestoreRW restores the rw layer from the checkpoint for the container func WithRestoreRW(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { diff --git a/images/mediatypes.go b/images/mediatypes.go index 005a2720b..186a3b673 100644 --- a/images/mediatypes.go +++ b/images/mediatypes.go @@ -34,7 +34,7 @@ const ( MediaTypeContainerd1Resource = "application/vnd.containerd.container.resource.tar" MediaTypeContainerd1RW = "application/vnd.containerd.container.rw.tar" MediaTypeContainerd1CheckpointConfig = "application/vnd.containerd.container.checkpoint.config.v1+proto" - MediaTypeContainerd1CheckpointImageName = "application/vnd.containerd.container.checkpoint.image.name" + MediaTypeContainerd1CheckpointOptions = "application/vnd.containerd.container.checkpoint.options.v1+proto" MediaTypeContainerd1CheckpointRuntimeName = "application/vnd.containerd.container.checkpoint.runtime.name" MediaTypeContainerd1CheckpointRuntimeOptions = "application/vnd.containerd.container.checkpoint.runtime.options+proto" // Legacy Docker schema1 manifest From 0e7a70dbfb00a109704e2f629539fc5064c55890 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 2 Oct 2018 15:59:43 +0000 Subject: [PATCH 9/9] skip already exists content Signed-off-by: Evan Hazlett --- container.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/container.go b/container.go index 26247592b..88898cc17 100644 --- a/container.go +++ b/container.go @@ -325,7 +325,10 @@ func (c *container) Checkpoint(ctx context.Context, ref string, opts ...Checkpoi // process remaining opts for _, o := range opts { if err := o(ctx, c.client, &info, index, copts); err != nil { - return nil, err + err = errdefs.FromGRPC(err) + if !errdefs.IsAlreadyExists(err) { + return nil, err + } } }