| // +build !windows |
| |
| package containerd |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "syscall" |
| |
| "github.com/containerd/containerd/api/types" |
| "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" |
| digest "github.com/opencontainers/go-digest" |
| "github.com/opencontainers/image-spec/identity" |
| "github.com/opencontainers/image-spec/specs-go/v1" |
| "github.com/pkg/errors" |
| "golang.org/x/sys/unix" |
| ) |
| |
| // 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() |
| id = desc.Digest |
| store = client.ContentStore() |
| ) |
| index, err := decodeIndex(ctx, store, id) |
| 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.Digest) |
| 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 |
| } |
| } |
| |
| // WithTaskCheckpoint allows a task to be created with live runtime and memory data from a |
| // previous checkpoint. Additional software such as CRIU may be required to |
| // restore a task from a checkpoint |
| func WithTaskCheckpoint(im Image) NewTaskOpts { |
| return func(ctx context.Context, c *Client, info *TaskInfo) error { |
| desc := im.Target() |
| id := desc.Digest |
| index, err := decodeIndex(ctx, c.ContentStore(), id) |
| if err != nil { |
| return err |
| } |
| for _, m := range index.Manifests { |
| if m.MediaType == images.MediaTypeContainerd1Checkpoint { |
| info.Checkpoint = &types.Descriptor{ |
| MediaType: m.MediaType, |
| Size_: m.Size, |
| Digest: m.Digest, |
| } |
| return nil |
| } |
| } |
| return fmt.Errorf("checkpoint not found in index %s", id) |
| } |
| } |
| |
| func decodeIndex(ctx context.Context, store content.Store, id digest.Digest) (*v1.Index, error) { |
| var index v1.Index |
| p, err := content.ReadBlob(ctx, store, id) |
| if err != nil { |
| return nil, err |
| } |
| if err := json.Unmarshal(p, &index); err != nil { |
| return nil, err |
| } |
| |
| return &index, 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 { |
| return withRemappedSnapshotBase(id, i, uid, gid, false) |
| } |
| |
| // WithRemappedSnapshotView is similar to WithRemappedSnapshot but rootfs is mounted as read-only. |
| func WithRemappedSnapshotView(id string, i Image, uid, gid uint32) NewContainerOpts { |
| return withRemappedSnapshotBase(id, i, uid, gid, true) |
| } |
| |
| func withRemappedSnapshotBase(id string, i Image, uid, gid uint32, readonly bool) NewContainerOpts { |
| return func(ctx context.Context, client *Client, c *containers.Container) error { |
| diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), platforms.Default()) |
| if err != nil { |
| return err |
| } |
| |
| setSnapshotterIfEmpty(c) |
| |
| var ( |
| snapshotter = client.SnapshotService(c.Snapshotter) |
| parent = identity.ChainID(diffIDs).String() |
| usernsID = fmt.Sprintf("%s-%d-%d", parent, uid, gid) |
| ) |
| if _, err := snapshotter.Stat(ctx, usernsID); err == nil { |
| if _, err := snapshotter.Prepare(ctx, id, usernsID); err == nil { |
| c.SnapshotKey = id |
| c.Image = i.Name() |
| return nil |
| } else if !errdefs.IsNotFound(err) { |
| return err |
| } |
| } |
| mounts, err := snapshotter.Prepare(ctx, usernsID+"-remap", parent) |
| if err != nil { |
| return err |
| } |
| if err := remapRootFS(mounts, uid, gid); err != nil { |
| snapshotter.Remove(ctx, usernsID) |
| return err |
| } |
| if err := snapshotter.Commit(ctx, usernsID, usernsID+"-remap"); err != nil { |
| return err |
| } |
| if readonly { |
| _, err = snapshotter.View(ctx, id, usernsID) |
| } else { |
| _, err = snapshotter.Prepare(ctx, id, usernsID) |
| } |
| if err != nil { |
| return err |
| } |
| c.SnapshotKey = id |
| c.Image = i.Name() |
| return nil |
| } |
| } |
| |
| func remapRootFS(mounts []mount.Mount, uid, gid uint32) error { |
| root, err := ioutil.TempDir("", "ctd-remap") |
| if err != nil { |
| return err |
| } |
| defer os.RemoveAll(root) |
| for _, m := range mounts { |
| if err := m.Mount(root); err != nil { |
| return err |
| } |
| } |
| defer unix.Unmount(root, 0) |
| return filepath.Walk(root, incrementFS(root, uid, gid)) |
| } |
| |
| func incrementFS(root string, uidInc, gidInc uint32) filepath.WalkFunc { |
| return func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| var ( |
| stat = info.Sys().(*syscall.Stat_t) |
| u, g = int(stat.Uid + uidInc), int(stat.Gid + gidInc) |
| ) |
| // be sure the lchown the path as to not de-reference the symlink to a host file |
| return os.Lchown(path, u, g) |
| } |
| } |