diff --git a/archive/tar.go b/archive/tar.go index ebdbb5f24..0fcfb6539 100644 --- a/archive/tar.go +++ b/archive/tar.go @@ -63,13 +63,34 @@ func Diff(ctx context.Context, a, b string) io.ReadCloser { } // WriteDiff writes a tar stream of the computed difference between the -// provided directories. +// provided paths. // // 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 { +func WriteDiff(ctx context.Context, w io.Writer, a, b string, opts ...WriteDiffOpt) error { + var options WriteDiffOptions + for _, opt := range opts { + if err := opt(&options); err != nil { + return errors.Wrap(err, "failed to apply option") + } + } + if options.writeDiffFunc == nil { + options.writeDiffFunc = writeDiffNaive + } + + return options.writeDiffFunc(ctx, w, a, b, options) +} + +// writeDiffNaive writes a tar stream of the computed difference between the +// provided directories on disk. +// +// 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 writeDiffNaive(ctx context.Context, w io.Writer, a, b string, _ WriteDiffOptions) error { cw := newChangeWriter(w, b) err := fs.Changes(ctx, a, b, cw.HandleChange) if err != nil { diff --git a/archive/tar_linux_test.go b/archive/tar_linux_test.go index f0984681a..2ef8d43a7 100644 --- a/archive/tar_linux_test.go +++ b/archive/tar_linux_test.go @@ -70,7 +70,7 @@ func TestOverlayApplyNoParents(t *testing.T) { } fstest.FSSuite(t, overlayDiffApplier{ tmp: base, - diff: func(ctx context.Context, w io.Writer, a, b string) error { + diff: func(ctx context.Context, w io.Writer, a, b string, _ ...WriteDiffOpt) error { cw := newChangeWriter(w, b) cw.addedDirs = nil err := fs.Changes(ctx, a, b, cw.HandleChange) @@ -85,7 +85,7 @@ func TestOverlayApplyNoParents(t *testing.T) { type overlayDiffApplier struct { tmp string - diff func(context.Context, io.Writer, string, string) error + diff func(context.Context, io.Writer, string, string, ...WriteDiffOpt) error t *testing.T } diff --git a/archive/tar_opts.go b/archive/tar_opts.go index 9f058f3a6..58985555a 100644 --- a/archive/tar_opts.go +++ b/archive/tar_opts.go @@ -73,3 +73,13 @@ func WithParents(p []string) ApplyOpt { return nil } } + +// WriteDiffOptions provides additional options for a WriteDiff operation +type WriteDiffOptions struct { + ParentLayers []string // Windows needs the full list of parent layers + + writeDiffFunc func(context.Context, io.Writer, string, string, WriteDiffOptions) error +} + +// WriteDiffOpt allows setting mutable archive write properties on creation +type WriteDiffOpt func(options *WriteDiffOptions) error diff --git a/archive/tar_opts_windows.go b/archive/tar_opts_windows.go index ce60602d5..d3b8f4fb9 100644 --- a/archive/tar_opts_windows.go +++ b/archive/tar_opts_windows.go @@ -40,3 +40,33 @@ func AsWindowsContainerLayer() ApplyOpt { return nil } } + +// writeDiffWindowsLayers writes a tar stream of the computed difference between the +// provided Windows layers +// +// 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 writeDiffWindowsLayers(ctx context.Context, w io.Writer, _, layer string, options WriteDiffOptions) error { + return ociwclayer.ExportLayerToTar(ctx, w, layer, options.ParentLayers) +} + +// AsWindowsContainerLayerPair indicates that the paths to diff are a pair of +// Windows Container Layers. The caller must be holding SeBackupPrivilege. +func AsWindowsContainerLayerPair() WriteDiffOpt { + return func(options *WriteDiffOptions) error { + options.writeDiffFunc = writeDiffWindowsLayers + return nil + } +} + +// WithParentLayers provides the Windows Container Layers that are the parents +// of the target (right-hand, "upper") layer, if any. The source (left-hand, "lower") +// layer passed to WriteDiff should be "" in this case. +func WithParentLayers(p []string) WriteDiffOpt { + return func(options *WriteDiffOptions) error { + options.ParentLayers = p + return nil + } +} diff --git a/diff/windows/windows.go b/diff/windows/windows.go index a50bbb6ce..7285d2945 100644 --- a/diff/windows/windows.go +++ b/diff/windows/windows.go @@ -20,12 +20,16 @@ package windows import ( "context" + "crypto/rand" + "encoding/base64" + "fmt" "io" "io/ioutil" "time" winio "github.com/Microsoft/go-winio" "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/archive/compression" "github.com/containerd/containerd/content" "github.com/containerd/containerd/diff" "github.com/containerd/containerd/errdefs" @@ -73,6 +77,7 @@ type windowsDiff struct { } var emptyDesc = ocispec.Descriptor{} +var uncompressed = "containerd.io/uncompressed" // NewWindowsDiff is the Windows container layer implementation // for comparing and applying filesystem layers @@ -135,6 +140,7 @@ func (s windowsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts // TODO darrenstahlmsft: When this is done isolated, we should disable these. // it currently cannot be disabled, unless we add ref counting. Since this is // temporary, leaving it enabled is OK for now. + // https://github.com/containerd/containerd/issues/1681 if err := winio.EnableProcessPrivileges([]string{winio.SeBackupPrivilege, winio.SeRestorePrivilege}); err != nil { return emptyDesc, err } @@ -158,7 +164,136 @@ func (s windowsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts // Compare creates a diff between the given mounts and uploads the result // to the content store. func (s windowsDiff) Compare(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) { - return emptyDesc, errors.Wrap(errdefs.ErrNotImplemented, "windowsDiff does not implement Compare method") + t1 := time.Now() + + var config diff.Config + for _, opt := range opts { + if err := opt(&config); err != nil { + return emptyDesc, err + } + } + + layers, err := mountPairToLayerStack(lower, upper) + if err != nil { + return emptyDesc, err + } + + if config.MediaType == "" { + config.MediaType = ocispec.MediaTypeImageLayerGzip + } + + var isCompressed bool + switch config.MediaType { + case ocispec.MediaTypeImageLayer: + case ocispec.MediaTypeImageLayerGzip: + isCompressed = true + default: + return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", config.MediaType) + } + + newReference := false + if config.Reference == "" { + newReference = true + config.Reference = uniqueRef() + } + + cw, err := s.store.Writer(ctx, content.WithRef(config.Reference), content.WithDescriptor(ocispec.Descriptor{ + MediaType: config.MediaType, + })) + + if err != nil { + return emptyDesc, errors.Wrap(err, "failed to open writer") + } + + defer func() { + if err != nil { + cw.Close() + if newReference { + if abortErr := s.store.Abort(ctx, config.Reference); abortErr != nil { + log.G(ctx).WithError(abortErr).WithField("ref", config.Reference).Warnf("failed to delete diff upload") + } + } + } + }() + + if !newReference { + if err = cw.Truncate(0); err != nil { + return emptyDesc, err + } + } + + // TODO darrenstahlmsft: When this is done isolated, we should disable this. + // it currently cannot be disabled, unless we add ref counting. Since this is + // temporary, leaving it enabled is OK for now. + // https://github.com/containerd/containerd/issues/1681 + if err := winio.EnableProcessPrivileges([]string{winio.SeBackupPrivilege}); err != nil { + return emptyDesc, err + } + + if isCompressed { + dgstr := digest.SHA256.Digester() + var compressed io.WriteCloser + compressed, err = compression.CompressStream(cw, compression.Gzip) + if err != nil { + return emptyDesc, errors.Wrap(err, "failed to get compressed stream") + } + err = archive.WriteDiff(ctx, io.MultiWriter(compressed, dgstr.Hash()), "", layers[0], archive.AsWindowsContainerLayerPair(), archive.WithParentLayers(layers[1:])) + compressed.Close() + if err != nil { + return emptyDesc, errors.Wrap(err, "failed to write compressed diff") + } + + if config.Labels == nil { + config.Labels = map[string]string{} + } + config.Labels[uncompressed] = dgstr.Digest().String() + } else { + if err = archive.WriteDiff(ctx, cw, "", layers[0], archive.AsWindowsContainerLayerPair(), archive.WithParentLayers(layers[1:])); err != nil { + return emptyDesc, errors.Wrap(err, "failed to write diff") + } + } + + var commitopts []content.Opt + if config.Labels != nil { + commitopts = append(commitopts, content.WithLabels(config.Labels)) + } + + dgst := cw.Digest() + if err := cw.Commit(ctx, 0, dgst, commitopts...); err != nil { + if !errdefs.IsAlreadyExists(err) { + return emptyDesc, errors.Wrap(err, "failed to commit") + } + } + + info, err := s.store.Info(ctx, dgst) + if err != nil { + return emptyDesc, errors.Wrap(err, "failed to get info from content store") + } + if info.Labels == nil { + info.Labels = make(map[string]string) + } + // Set uncompressed label if digest already existed without label + if _, ok := info.Labels[uncompressed]; !ok { + info.Labels[uncompressed] = config.Labels[uncompressed] + if _, err := s.store.Update(ctx, info, "labels."+uncompressed); err != nil { + return emptyDesc, errors.Wrap(err, "error setting uncompressed label") + } + } + + desc := ocispec.Descriptor{ + MediaType: config.MediaType, + Size: info.Size, + Digest: info.Digest, + } + + log.G(ctx).WithFields(logrus.Fields{ + "d": time.Since(t1), + "dgst": desc.Digest, + "size": desc.Size, + "media": desc.MediaType, + }).Debug("diff created") + + return desc, nil } type readCounter struct { @@ -191,3 +326,58 @@ func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) { return mnt.Source, parentLayerPaths, nil } + +// mountPairToLayerStack ensures that the two sets of mount-lists are actually a correct +// parent-and-child, or orphan-and-empty-list, and return the full list of layers, starting +// with the upper-most (most childish?) +func mountPairToLayerStack(lower, upper []mount.Mount) ([]string, error) { + + // May return an ErrNotImplemented, which will fall back to LCOW + upperLayer, upperParentLayerPaths, err := mountsToLayerAndParents(upper) + if err != nil { + return nil, errors.Wrapf(err, "Upper mount invalid") + } + + // Trivial case, diff-against-nothing + if len(lower) == 0 { + if len(upperParentLayerPaths) != 0 { + return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer with parents against a null layer") + } + return []string{upperLayer}, nil + } + + if len(upperParentLayerPaths) < 1 { + return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer with no parents against another layer") + } + + lowerLayer, lowerParentLayerPaths, err := mountsToLayerAndParents(lower) + if errdefs.IsNotImplemented(err) { + // Upper was a windows-layer, lower is not. We can't handle that. + return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a windows-layer against a non-windows-layer") + } else if err != nil { + return nil, errors.Wrapf(err, "Lower mount invalid") + } + + if upperParentLayerPaths[0] != lowerLayer { + return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer against a layer other than its own parent") + } + + if len(upperParentLayerPaths) != len(lowerParentLayerPaths)+1 { + return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer against a layer with different parents") + } + for i, upperParent := range upperParentLayerPaths[1:] { + if upperParent != lowerParentLayerPaths[i] { + return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer against a layer with different parents") + } + } + + return append([]string{upperLayer}, upperParentLayerPaths...), nil +} + +func uniqueRef() string { + t := time.Now() + var b [3]byte + // Ignore read failures, just decreases uniqueness + rand.Read(b[:]) + return fmt.Sprintf("%d-%s", t.UnixNano(), base64.URLEncoding.EncodeToString(b[:])) +} diff --git a/integration/client/export_test.go b/integration/client/export_test.go index a2c97374e..1e661b99b 100644 --- a/integration/client/export_test.go +++ b/integration/client/export_test.go @@ -20,7 +20,6 @@ import ( "archive/tar" "bytes" "io" - "runtime" "testing" . "github.com/containerd/containerd" @@ -30,8 +29,7 @@ import ( // TestExport exports testImage as a tar stream func TestExport(t *testing.T) { - // TODO: support windows - if testing.Short() || runtime.GOOS == "windows" { + if testing.Short() { t.Skip() } ctx, cancel := testContext(t) diff --git a/integration/client/import_test.go b/integration/client/import_test.go index 6a1415164..22d5603a7 100644 --- a/integration/client/import_test.go +++ b/integration/client/import_test.go @@ -25,7 +25,6 @@ import ( "io/ioutil" "math/rand" "reflect" - "runtime" "testing" . "github.com/containerd/containerd" @@ -41,8 +40,7 @@ import ( // TestExportAndImport exports testImage as a tar stream, // and import the tar stream as a new image. func TestExportAndImport(t *testing.T) { - // TODO: support windows - if testing.Short() || runtime.GOOS == "windows" { + if testing.Short() { t.Skip() } ctx, cancel := testContext(t)