Skip to content

Commit c750498

Browse files
committed
Implement windowsDiff.Compare via hcsshim/pkg/ociwclayer
This parallels the implementation of windowsDiff.Apply, including bouncing very briefly though archive.WriteDiff and then straight back out into Windows-specific code. It's mostly pulling existing mechanisms from non-Windows Compare or Windows Apply, and highlights that there's probably a lot of scope for refactoring on top of this. Now the export-related integration tests pass CI on Windows. Signed-off-by: Paul "TBBle" Hampson <[email protected]>
1 parent a64a768 commit c750498

7 files changed

Lines changed: 258 additions & 11 deletions

File tree

archive/tar.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,34 @@ func Diff(ctx context.Context, a, b string) io.ReadCloser {
6363
}
6464

6565
// WriteDiff writes a tar stream of the computed difference between the
66-
// provided directories.
66+
// provided paths.
6767
//
6868
// Produces a tar using OCI style file markers for deletions. Deleted
6969
// files will be prepended with the prefix ".wh.". This style is
7070
// based off AUFS whiteouts.
7171
// See https://github.com/opencontainers/image-spec/blob/master/layer.md
72-
func WriteDiff(ctx context.Context, w io.Writer, a, b string) error {
72+
func WriteDiff(ctx context.Context, w io.Writer, a, b string, opts ...WriteDiffOpt) error {
73+
var options WriteDiffOptions
74+
for _, opt := range opts {
75+
if err := opt(&options); err != nil {
76+
return errors.Wrap(err, "failed to apply option")
77+
}
78+
}
79+
if options.writeDiffFunc == nil {
80+
options.writeDiffFunc = writeDiffNaive
81+
}
82+
83+
return options.writeDiffFunc(ctx, w, a, b, options)
84+
}
85+
86+
// writeDiffNaive writes a tar stream of the computed difference between the
87+
// provided directories on disk.
88+
//
89+
// Produces a tar using OCI style file markers for deletions. Deleted
90+
// files will be prepended with the prefix ".wh.". This style is
91+
// based off AUFS whiteouts.
92+
// See https://github.com/opencontainers/image-spec/blob/master/layer.md
93+
func writeDiffNaive(ctx context.Context, w io.Writer, a, b string, _ WriteDiffOptions) error {
7394
cw := newChangeWriter(w, b)
7495
err := fs.Changes(ctx, a, b, cw.HandleChange)
7596
if err != nil {

archive/tar_linux_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestOverlayApplyNoParents(t *testing.T) {
7070
}
7171
fstest.FSSuite(t, overlayDiffApplier{
7272
tmp: base,
73-
diff: func(ctx context.Context, w io.Writer, a, b string) error {
73+
diff: func(ctx context.Context, w io.Writer, a, b string, _ ...WriteDiffOpt) error {
7474
cw := newChangeWriter(w, b)
7575
cw.addedDirs = nil
7676
err := fs.Changes(ctx, a, b, cw.HandleChange)
@@ -85,7 +85,7 @@ func TestOverlayApplyNoParents(t *testing.T) {
8585

8686
type overlayDiffApplier struct {
8787
tmp string
88-
diff func(context.Context, io.Writer, string, string) error
88+
diff func(context.Context, io.Writer, string, string, ...WriteDiffOpt) error
8989
t *testing.T
9090
}
9191

archive/tar_opts.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,13 @@ func WithParents(p []string) ApplyOpt {
7373
return nil
7474
}
7575
}
76+
77+
// WriteDiffOptions provides additional options for a WriteDiff operation
78+
type WriteDiffOptions struct {
79+
ParentLayers []string // Windows needs the full list of parent layers
80+
81+
writeDiffFunc func(context.Context, io.Writer, string, string, WriteDiffOptions) error
82+
}
83+
84+
// WriteDiffOpt allows setting mutable archive write properties on creation
85+
type WriteDiffOpt func(options *WriteDiffOptions) error

archive/tar_opts_windows.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,33 @@ func AsWindowsContainerLayer() ApplyOpt {
4040
return nil
4141
}
4242
}
43+
44+
// writeDiffWindowsLayers writes a tar stream of the computed difference between the
45+
// provided Windows layers
46+
//
47+
// Produces a tar using OCI style file markers for deletions. Deleted
48+
// files will be prepended with the prefix ".wh.". This style is
49+
// based off AUFS whiteouts.
50+
// See https://github.com/opencontainers/image-spec/blob/master/layer.md
51+
func writeDiffWindowsLayers(ctx context.Context, w io.Writer, _, layer string, options WriteDiffOptions) error {
52+
return ociwclayer.ExportLayerToTar(ctx, w, layer, options.ParentLayers)
53+
}
54+
55+
// AsWindowsContainerLayerPair indicates that the paths to diff are a pair of
56+
// Windows Container Layers. The caller must be holding SeBackupPrivilege.
57+
func AsWindowsContainerLayerPair() WriteDiffOpt {
58+
return func(options *WriteDiffOptions) error {
59+
options.writeDiffFunc = writeDiffWindowsLayers
60+
return nil
61+
}
62+
}
63+
64+
// WithParentLayers provides the Windows Container Layers that are the parents
65+
// of the target (right-hand, "upper") layer, if any. The source (left-hand, "lower")
66+
// layer passed to WriteDiff should be "" in this case.
67+
func WithParentLayers(p []string) WriteDiffOpt {
68+
return func(options *WriteDiffOptions) error {
69+
options.ParentLayers = p
70+
return nil
71+
}
72+
}

diff/windows/windows.go

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ package windows
2020

2121
import (
2222
"context"
23+
"crypto/rand"
24+
"encoding/base64"
25+
"fmt"
2326
"io"
2427
"io/ioutil"
2528
"time"
2629

2730
winio "github.com/Microsoft/go-winio"
2831
"github.com/containerd/containerd/archive"
32+
"github.com/containerd/containerd/archive/compression"
2933
"github.com/containerd/containerd/content"
3034
"github.com/containerd/containerd/diff"
3135
"github.com/containerd/containerd/errdefs"
@@ -73,6 +77,7 @@ type windowsDiff struct {
7377
}
7478

7579
var emptyDesc = ocispec.Descriptor{}
80+
var uncompressed = "containerd.io/uncompressed"
7681

7782
// NewWindowsDiff is the Windows container layer implementation
7883
// for comparing and applying filesystem layers
@@ -135,6 +140,7 @@ func (s windowsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts
135140
// TODO darrenstahlmsft: When this is done isolated, we should disable these.
136141
// it currently cannot be disabled, unless we add ref counting. Since this is
137142
// temporary, leaving it enabled is OK for now.
143+
// https://github.com/containerd/containerd/issues/1681
138144
if err := winio.EnableProcessPrivileges([]string{winio.SeBackupPrivilege, winio.SeRestorePrivilege}); err != nil {
139145
return emptyDesc, err
140146
}
@@ -158,7 +164,136 @@ func (s windowsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts
158164
// Compare creates a diff between the given mounts and uploads the result
159165
// to the content store.
160166
func (s windowsDiff) Compare(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) {
161-
return emptyDesc, errors.Wrap(errdefs.ErrNotImplemented, "windowsDiff does not implement Compare method")
167+
t1 := time.Now()
168+
169+
var config diff.Config
170+
for _, opt := range opts {
171+
if err := opt(&config); err != nil {
172+
return emptyDesc, err
173+
}
174+
}
175+
176+
layers, err := mountPairToLayerStack(lower, upper)
177+
if err != nil {
178+
return emptyDesc, err
179+
}
180+
181+
if config.MediaType == "" {
182+
config.MediaType = ocispec.MediaTypeImageLayerGzip
183+
}
184+
185+
var isCompressed bool
186+
switch config.MediaType {
187+
case ocispec.MediaTypeImageLayer:
188+
case ocispec.MediaTypeImageLayerGzip:
189+
isCompressed = true
190+
default:
191+
return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", config.MediaType)
192+
}
193+
194+
newReference := false
195+
if config.Reference == "" {
196+
newReference = true
197+
config.Reference = uniqueRef()
198+
}
199+
200+
cw, err := s.store.Writer(ctx, content.WithRef(config.Reference), content.WithDescriptor(ocispec.Descriptor{
201+
MediaType: config.MediaType,
202+
}))
203+
204+
if err != nil {
205+
return emptyDesc, errors.Wrap(err, "failed to open writer")
206+
}
207+
208+
defer func() {
209+
if err != nil {
210+
cw.Close()
211+
if newReference {
212+
if abortErr := s.store.Abort(ctx, config.Reference); abortErr != nil {
213+
log.G(ctx).WithError(abortErr).WithField("ref", config.Reference).Warnf("failed to delete diff upload")
214+
}
215+
}
216+
}
217+
}()
218+
219+
if !newReference {
220+
if err = cw.Truncate(0); err != nil {
221+
return emptyDesc, err
222+
}
223+
}
224+
225+
// TODO darrenstahlmsft: When this is done isolated, we should disable this.
226+
// it currently cannot be disabled, unless we add ref counting. Since this is
227+
// temporary, leaving it enabled is OK for now.
228+
// https://github.com/containerd/containerd/issues/1681
229+
if err := winio.EnableProcessPrivileges([]string{winio.SeBackupPrivilege}); err != nil {
230+
return emptyDesc, err
231+
}
232+
233+
if isCompressed {
234+
dgstr := digest.SHA256.Digester()
235+
var compressed io.WriteCloser
236+
compressed, err = compression.CompressStream(cw, compression.Gzip)
237+
if err != nil {
238+
return emptyDesc, errors.Wrap(err, "failed to get compressed stream")
239+
}
240+
err = archive.WriteDiff(ctx, io.MultiWriter(compressed, dgstr.Hash()), "", layers[0], archive.AsWindowsContainerLayerPair(), archive.WithParentLayers(layers[1:]))
241+
compressed.Close()
242+
if err != nil {
243+
return emptyDesc, errors.Wrap(err, "failed to write compressed diff")
244+
}
245+
246+
if config.Labels == nil {
247+
config.Labels = map[string]string{}
248+
}
249+
config.Labels[uncompressed] = dgstr.Digest().String()
250+
} else {
251+
if err = archive.WriteDiff(ctx, cw, "", layers[0], archive.AsWindowsContainerLayerPair(), archive.WithParentLayers(layers[1:])); err != nil {
252+
return emptyDesc, errors.Wrap(err, "failed to write diff")
253+
}
254+
}
255+
256+
var commitopts []content.Opt
257+
if config.Labels != nil {
258+
commitopts = append(commitopts, content.WithLabels(config.Labels))
259+
}
260+
261+
dgst := cw.Digest()
262+
if err := cw.Commit(ctx, 0, dgst, commitopts...); err != nil {
263+
if !errdefs.IsAlreadyExists(err) {
264+
return emptyDesc, errors.Wrap(err, "failed to commit")
265+
}
266+
}
267+
268+
info, err := s.store.Info(ctx, dgst)
269+
if err != nil {
270+
return emptyDesc, errors.Wrap(err, "failed to get info from content store")
271+
}
272+
if info.Labels == nil {
273+
info.Labels = make(map[string]string)
274+
}
275+
// Set uncompressed label if digest already existed without label
276+
if _, ok := info.Labels[uncompressed]; !ok {
277+
info.Labels[uncompressed] = config.Labels[uncompressed]
278+
if _, err := s.store.Update(ctx, info, "labels."+uncompressed); err != nil {
279+
return emptyDesc, errors.Wrap(err, "error setting uncompressed label")
280+
}
281+
}
282+
283+
desc := ocispec.Descriptor{
284+
MediaType: config.MediaType,
285+
Size: info.Size,
286+
Digest: info.Digest,
287+
}
288+
289+
log.G(ctx).WithFields(logrus.Fields{
290+
"d": time.Since(t1),
291+
"dgst": desc.Digest,
292+
"size": desc.Size,
293+
"media": desc.MediaType,
294+
}).Debug("diff created")
295+
296+
return desc, nil
162297
}
163298

164299
type readCounter struct {
@@ -191,3 +326,58 @@ func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) {
191326

192327
return mnt.Source, parentLayerPaths, nil
193328
}
329+
330+
// mountPairToLayerStack ensures that the two sets of mount-lists are actually a correct
331+
// parent-and-child, or orphan-and-empty-list, and return the full list of layers, starting
332+
// with the upper-most (most childish?)
333+
func mountPairToLayerStack(lower, upper []mount.Mount) ([]string, error) {
334+
335+
// May return an ErrNotImplemented, which will fall back to LCOW
336+
upperLayer, upperParentLayerPaths, err := mountsToLayerAndParents(upper)
337+
if err != nil {
338+
return nil, errors.Wrapf(err, "Upper mount invalid")
339+
}
340+
341+
// Trivial case, diff-against-nothing
342+
if len(lower) == 0 {
343+
if len(upperParentLayerPaths) != 0 {
344+
return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer with parents against a null layer")
345+
}
346+
return []string{upperLayer}, nil
347+
}
348+
349+
if len(upperParentLayerPaths) < 1 {
350+
return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer with no parents against another layer")
351+
}
352+
353+
lowerLayer, lowerParentLayerPaths, err := mountsToLayerAndParents(lower)
354+
if errdefs.IsNotImplemented(err) {
355+
// Upper was a windows-layer, lower is not. We can't handle that.
356+
return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a windows-layer against a non-windows-layer")
357+
} else if err != nil {
358+
return nil, errors.Wrapf(err, "Lower mount invalid")
359+
}
360+
361+
if upperParentLayerPaths[0] != lowerLayer {
362+
return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer against a layer other than its own parent")
363+
}
364+
365+
if len(upperParentLayerPaths) != len(lowerParentLayerPaths)+1 {
366+
return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer against a layer with different parents")
367+
}
368+
for i, upperParent := range upperParentLayerPaths[1:] {
369+
if upperParent != lowerParentLayerPaths[i] {
370+
return nil, errors.Wrap(errdefs.ErrInvalidArgument, "windowsDiff cannot diff a layer against a layer with different parents")
371+
}
372+
}
373+
374+
return append([]string{upperLayer}, upperParentLayerPaths...), nil
375+
}
376+
377+
func uniqueRef() string {
378+
t := time.Now()
379+
var b [3]byte
380+
// Ignore read failures, just decreases uniqueness
381+
rand.Read(b[:])
382+
return fmt.Sprintf("%d-%s", t.UnixNano(), base64.URLEncoding.EncodeToString(b[:]))
383+
}

integration/client/export_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"archive/tar"
2121
"bytes"
2222
"io"
23-
"runtime"
2423
"testing"
2524

2625
. "github.com/containerd/containerd"
@@ -30,8 +29,7 @@ import (
3029

3130
// TestExport exports testImage as a tar stream
3231
func TestExport(t *testing.T) {
33-
// TODO: support windows
34-
if testing.Short() || runtime.GOOS == "windows" {
32+
if testing.Short() {
3533
t.Skip()
3634
}
3735
ctx, cancel := testContext(t)

integration/client/import_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"io/ioutil"
2626
"math/rand"
2727
"reflect"
28-
"runtime"
2928
"testing"
3029

3130
. "github.com/containerd/containerd"
@@ -41,8 +40,7 @@ import (
4140
// TestExportAndImport exports testImage as a tar stream,
4241
// and import the tar stream as a new image.
4342
func TestExportAndImport(t *testing.T) {
44-
// TODO: support windows
45-
if testing.Short() || runtime.GOOS == "windows" {
43+
if testing.Short() {
4644
t.Skip()
4745
}
4846
ctx, cancel := testContext(t)

0 commit comments

Comments
 (0)