Add diff service implementation
Add snapshot subcommand to ctr for creating diffs of RW layers. Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
parent
47718b0930
commit
3ae69c43d8
@ -40,13 +40,7 @@ func Diff(ctx context.Context, a, b string) io.ReadCloser {
|
|||||||
r, w := io.Pipe()
|
r, w := io.Pipe()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
err := WriteDiff(ctx, w, a, b)
|
||||||
cw := newChangeWriter(w, b)
|
|
||||||
if err = fs.Changes(ctx, a, b, cw.HandleChange); err != nil {
|
|
||||||
err = errors.Wrap(err, "failed to create diff tar stream")
|
|
||||||
} else {
|
|
||||||
err = cw.Close()
|
|
||||||
}
|
|
||||||
if err = w.CloseWithError(err); err != nil {
|
if err = w.CloseWithError(err); err != nil {
|
||||||
log.G(ctx).WithError(err).Debugf("closing tar pipe failed")
|
log.G(ctx).WithError(err).Debugf("closing tar pipe failed")
|
||||||
}
|
}
|
||||||
@ -55,6 +49,22 @@ func Diff(ctx context.Context, a, b string) io.ReadCloser {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteDiff writes a tar stream of the computed difference between the
|
||||||
|
// provided directories.
|
||||||
|
//
|
||||||
|
// Produces a tar using OCI style file markers for deletions. Deleted
|
||||||
|
// files will be prepended with the prefix ".wh.". This style is
|
||||||
|
// based off AUFS whiteouts.
|
||||||
|
// See https://github.com/opencontainers/image-spec/blob/master/layer.md
|
||||||
|
func WriteDiff(ctx context.Context, w io.Writer, a, b string) error {
|
||||||
|
cw := newChangeWriter(w, b)
|
||||||
|
err := fs.Changes(ctx, a, b, cw.HandleChange)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create diff tar stream")
|
||||||
|
}
|
||||||
|
return cw.Close()
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// whiteoutPrefix prefix means file is a whiteout. If this is followed by a
|
// whiteoutPrefix prefix means file is a whiteout. If this is followed by a
|
||||||
// filename this means that file has been removed from the base layer.
|
// filename this means that file has been removed from the base layer.
|
||||||
|
@ -61,6 +61,7 @@ containerd client
|
|||||||
execCommand,
|
execCommand,
|
||||||
pauseCommand,
|
pauseCommand,
|
||||||
resumeCommand,
|
resumeCommand,
|
||||||
|
snapshotCommand,
|
||||||
versionCommand,
|
versionCommand,
|
||||||
}
|
}
|
||||||
app.Commands = append(app.Commands, extraCmds...)
|
app.Commands = append(app.Commands, extraCmds...)
|
||||||
|
@ -54,6 +54,10 @@ var runCommand = cli.Command{
|
|||||||
Name: "net-host",
|
Name: "net-host",
|
||||||
Usage: "enable host networking for the container",
|
Usage: "enable host networking for the container",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "keep",
|
||||||
|
Usage: "keep container after running",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(context *cli.Context) error {
|
Action: func(context *cli.Context) error {
|
||||||
var (
|
var (
|
||||||
@ -110,7 +114,11 @@ var runCommand = cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mounts, err = snapshotter.Prepare(ctx, id, identity.ChainID(diffIDs).String())
|
if context.Bool("readonly") {
|
||||||
|
mounts, err = snapshotter.View(ctx, id, identity.ChainID(diffIDs).String())
|
||||||
|
} else {
|
||||||
|
mounts, err = snapshotter.Prepare(ctx, id, identity.ChainID(diffIDs).String())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !snapshot.IsExist(err) {
|
if !snapshot.IsExist(err) {
|
||||||
return err
|
return err
|
||||||
@ -192,10 +200,12 @@ var runCommand = cli.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := containers.Delete(ctx, &execution.DeleteRequest{
|
if !context.Bool("keep") {
|
||||||
ID: response.ID,
|
if _, err := containers.Delete(ctx, &execution.DeleteRequest{
|
||||||
}); err != nil {
|
ID: response.ID,
|
||||||
return err
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
return cli.NewExitError("", int(status))
|
return cli.NewExitError("", int(status))
|
||||||
|
50
cmd/ctr/snapshot.go
Normal file
50
cmd/ctr/snapshot.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/rootfs"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var snapshotCommand = cli.Command{
|
||||||
|
Name: "snapshot",
|
||||||
|
Usage: "snapshot a container into an archive",
|
||||||
|
ArgsUsage: "",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "id",
|
||||||
|
Usage: "id of the container",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(clicontext *cli.Context) error {
|
||||||
|
id := clicontext.String("id")
|
||||||
|
if id == "" {
|
||||||
|
return errors.New("container id must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotter, err := getSnapshotter(clicontext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
differ, err := getDiffService(clicontext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentRef := fmt.Sprintf("diff-%s", id)
|
||||||
|
|
||||||
|
d, err := rootfs.Diff(context.TODO(), id, contentRef, snapshotter, differ)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Track progress
|
||||||
|
fmt.Printf("%s %s\n", d.MediaType, d.Digest)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
contentapi "github.com/containerd/containerd/api/services/content"
|
contentapi "github.com/containerd/containerd/api/services/content"
|
||||||
|
diffapi "github.com/containerd/containerd/api/services/diff"
|
||||||
"github.com/containerd/containerd/api/services/execution"
|
"github.com/containerd/containerd/api/services/execution"
|
||||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||||
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
|
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
|
||||||
@ -21,6 +22,7 @@ import (
|
|||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
contentservice "github.com/containerd/containerd/services/content"
|
contentservice "github.com/containerd/containerd/services/content"
|
||||||
|
"github.com/containerd/containerd/services/diff"
|
||||||
imagesservice "github.com/containerd/containerd/services/images"
|
imagesservice "github.com/containerd/containerd/services/images"
|
||||||
snapshotservice "github.com/containerd/containerd/services/snapshot"
|
snapshotservice "github.com/containerd/containerd/services/snapshot"
|
||||||
"github.com/containerd/containerd/snapshot"
|
"github.com/containerd/containerd/snapshot"
|
||||||
@ -62,6 +64,14 @@ func getImageStore(clicontext *cli.Context) (images.Store, error) {
|
|||||||
return imagesservice.NewStoreFromClient(imagesapi.NewImagesClient(conn)), nil
|
return imagesservice.NewStoreFromClient(imagesapi.NewImagesClient(conn)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDiffService(context *cli.Context) (diff.DiffService, error) {
|
||||||
|
conn, err := getGRPCConnection(context)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return diff.NewDiffServiceFromClient(diffapi.NewDiffClient(conn)), nil
|
||||||
|
}
|
||||||
|
|
||||||
func getVersionService(context *cli.Context) (versionservice.VersionClient, error) {
|
func getVersionService(context *cli.Context) (versionservice.VersionClient, error) {
|
||||||
conn, err := getGRPCConnection(context)
|
conn, err := getGRPCConnection(context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
2
cmd/dist/pull.go
vendored
2
cmd/dist/pull.go
vendored
@ -116,7 +116,7 @@ command. As part of this process, we do the following:
|
|||||||
log.G(ctx).Fatal(err)
|
log.G(ctx).Fatal(err)
|
||||||
}
|
}
|
||||||
snapshotter := snapshotservice.NewSnapshotterFromClient(snapshotapi.NewSnapshotClient(conn))
|
snapshotter := snapshotservice.NewSnapshotterFromClient(snapshotapi.NewSnapshotClient(conn))
|
||||||
applier := diffservice.NewApplierFromClient(diffapi.NewDiffClient(conn))
|
applier := diffservice.NewDiffServiceFromClient(diffapi.NewDiffClient(conn))
|
||||||
|
|
||||||
log.G(ctx).Info("unpacking rootfs")
|
log.G(ctx).Info("unpacking rootfs")
|
||||||
|
|
||||||
|
2
cmd/dist/rootfs.go
vendored
2
cmd/dist/rootfs.go
vendored
@ -65,7 +65,7 @@ var rootfsUnpackCommand = cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
snapshotter := snapshotservice.NewSnapshotterFromClient(snapshotapi.NewSnapshotClient(conn))
|
snapshotter := snapshotservice.NewSnapshotterFromClient(snapshotapi.NewSnapshotClient(conn))
|
||||||
applier := diffservice.NewApplierFromClient(diffapi.NewDiffClient(conn))
|
applier := diffservice.NewDiffServiceFromClient(diffapi.NewDiffClient(conn))
|
||||||
|
|
||||||
chainID, err := rootfs.ApplyLayers(ctx, layers, snapshotter, applier)
|
chainID, err := rootfs.ApplyLayers(ctx, layers, snapshotter, applier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
52
rootfs/diff.go
Normal file
52
rootfs/diff.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package rootfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd"
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
|
"github.com/containerd/containerd/snapshot"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MountDiffer interface {
|
||||||
|
DiffMounts(ctx context.Context, lower, upper []containerd.Mount, media, ref string) (ocispec.Descriptor, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiffOptions struct {
|
||||||
|
MountDiffer
|
||||||
|
content.Store
|
||||||
|
snapshot.Snapshotter
|
||||||
|
}
|
||||||
|
|
||||||
|
func Diff(ctx context.Context, snapshotID, contentRef string, sn snapshot.Snapshotter, md MountDiffer) (ocispec.Descriptor, error) {
|
||||||
|
info, err := sn.Stat(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
return ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerKey := fmt.Sprintf("%s-parent-view", info.Parent)
|
||||||
|
lower, err := sn.View(ctx, lowerKey, info.Parent)
|
||||||
|
if err != nil {
|
||||||
|
return ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
defer sn.Remove(ctx, lowerKey)
|
||||||
|
|
||||||
|
var upper []containerd.Mount
|
||||||
|
if info.Kind == snapshot.KindActive {
|
||||||
|
upper, err = sn.Mounts(ctx, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
return ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
upperKey := fmt.Sprintf("%s-view", snapshotID)
|
||||||
|
upper, err = sn.View(ctx, upperKey, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
return ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
defer sn.Remove(ctx, lowerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return md.DiffMounts(ctx, lower, upper, ocispec.MediaTypeImageLayer, contentRef)
|
||||||
|
}
|
@ -11,19 +11,24 @@ import (
|
|||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DiffService interface {
|
||||||
|
rootfs.Applier
|
||||||
|
rootfs.MountDiffer
|
||||||
|
}
|
||||||
|
|
||||||
// NewApplierFromClient returns a new Applier which communicates
|
// NewApplierFromClient returns a new Applier which communicates
|
||||||
// over a GRPC connection.
|
// over a GRPC connection.
|
||||||
func NewApplierFromClient(client diffapi.DiffClient) rootfs.Applier {
|
func NewDiffServiceFromClient(client diffapi.DiffClient) DiffService {
|
||||||
return &remoteApplier{
|
return &remote{
|
||||||
client: client,
|
client: client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type remoteApplier struct {
|
type remote struct {
|
||||||
client diffapi.DiffClient
|
client diffapi.DiffClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteApplier) Apply(ctx context.Context, diff ocispec.Descriptor, mounts []containerd.Mount) (ocispec.Descriptor, error) {
|
func (r *remote) Apply(ctx context.Context, diff ocispec.Descriptor, mounts []containerd.Mount) (ocispec.Descriptor, error) {
|
||||||
req := &diffapi.ApplyRequest{
|
req := &diffapi.ApplyRequest{
|
||||||
Diff: fromDescriptor(diff),
|
Diff: fromDescriptor(diff),
|
||||||
Mounts: fromMounts(mounts),
|
Mounts: fromMounts(mounts),
|
||||||
@ -35,6 +40,20 @@ func (r *remoteApplier) Apply(ctx context.Context, diff ocispec.Descriptor, moun
|
|||||||
return toDescriptor(resp.Applied), nil
|
return toDescriptor(resp.Applied), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *remote) DiffMounts(ctx context.Context, a, b []containerd.Mount, media, ref string) (ocispec.Descriptor, error) {
|
||||||
|
req := &diffapi.DiffRequest{
|
||||||
|
Left: fromMounts(a),
|
||||||
|
Right: fromMounts(b),
|
||||||
|
MediaType: media,
|
||||||
|
Ref: ref,
|
||||||
|
}
|
||||||
|
resp, err := r.client.Diff(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
return toDescriptor(resp.Diff), nil
|
||||||
|
}
|
||||||
|
|
||||||
func fromDescriptor(d ocispec.Descriptor) *descriptor.Descriptor {
|
func fromDescriptor(d ocispec.Descriptor) *descriptor.Descriptor {
|
||||||
return &descriptor.Descriptor{
|
return &descriptor.Descriptor{
|
||||||
MediaType: d.MediaType,
|
MediaType: d.MediaType,
|
||||||
|
@ -102,8 +102,73 @@ func (s *service) Apply(ctx context.Context, er *diffapi.ApplyRequest) (*diffapi
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Diff(context.Context, *diffapi.DiffRequest) (*diffapi.DiffResponse, error) {
|
func (s *service) Diff(ctx context.Context, dr *diffapi.DiffRequest) (*diffapi.DiffResponse, error) {
|
||||||
return nil, errors.New("not implemented")
|
aMounts := toMounts(dr.Left)
|
||||||
|
bMounts := toMounts(dr.Right)
|
||||||
|
|
||||||
|
aDir, err := ioutil.TempDir("", "left-")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create temporary directory")
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(aDir)
|
||||||
|
|
||||||
|
bDir, err := ioutil.TempDir("", "right-")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create temporary directory")
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(bDir)
|
||||||
|
|
||||||
|
if err := containerd.MountAll(aMounts, aDir); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to mount")
|
||||||
|
}
|
||||||
|
defer containerd.Unmount(aDir, 0)
|
||||||
|
|
||||||
|
if err := containerd.MountAll(bMounts, bDir); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to mount")
|
||||||
|
}
|
||||||
|
defer containerd.Unmount(bDir, 0)
|
||||||
|
|
||||||
|
cw, err := s.store.Writer(ctx, dr.Ref, 0, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to open writer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Validate media type
|
||||||
|
|
||||||
|
// TODO: Support compressed media types (link compressed to uncompressed)
|
||||||
|
//dgstr := digest.SHA256.Digester()
|
||||||
|
//wc := &writeCounter{}
|
||||||
|
//compressed, err := compression.CompressStream(cw, compression.Gzip)
|
||||||
|
//if err != nil {
|
||||||
|
// return nil, errors.Wrap(err, "failed to get compressed stream")
|
||||||
|
//}
|
||||||
|
//err = archive.WriteDiff(ctx, io.MultiWriter(compressed, dgstr.Hash(), wc), lowerDir, upperDir)
|
||||||
|
//compressed.Close()
|
||||||
|
|
||||||
|
err = archive.WriteDiff(ctx, cw, aDir, bDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to write diff")
|
||||||
|
}
|
||||||
|
|
||||||
|
dgst := cw.Digest()
|
||||||
|
if err := cw.Commit(0, dgst); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to commit")
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := s.store.Info(ctx, dgst)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get info from content store")
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := ocispec.Descriptor{
|
||||||
|
MediaType: dr.MediaType,
|
||||||
|
Digest: info.Digest,
|
||||||
|
Size: info.Size,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &diffapi.DiffResponse{
|
||||||
|
Diff: fromDescriptor(desc),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type readCounter struct {
|
type readCounter struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user