Skip to content

Commit 02a00c3

Browse files
author
Kazuyoshi Kato
authored
Merge pull request #8616 from gabriel-samfira/backport-8043
[release/1.7 backport] Mount snapshots on Windows
2 parents a04e94b + 313c226 commit 02a00c3

41 files changed

Lines changed: 951 additions & 300 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ jobs:
322322
ENABLE_CRI_SANDBOXES: ${{ matrix.enable_cri_sandboxes }}
323323
GOTESTSUM_JUNITFILE: ${{github.workspace}}/test-integration-serial-junit.xml
324324
GOTESTSUM_JSONFILE: ${{github.workspace}}/test-integration-serial-gotest.json
325+
EXTRA_TESTFLAGS: "-timeout=20m"
325326
run: mingw32-make.exe integration
326327
- run: if [ -f *-gotest.json ]; then echo '# Integration 1' >> $GITHUB_STEP_SUMMARY; teststat -markdown *-gotest.json >> $GITHUB_STEP_SUMMARY; fi
327328
if: always()

diff/windows/windows.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) {
320320
return "", nil, fmt.Errorf("number of mounts should always be 1 for Windows layers: %w", errdefs.ErrInvalidArgument)
321321
}
322322
mnt := mounts[0]
323+
323324
if mnt.Type != "windows-layer" {
324325
// This is a special case error. When this is received the diff service
325326
// will attempt the next differ in the chain which for Windows is the
@@ -332,6 +333,17 @@ func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) {
332333
return "", nil, err
333334
}
334335

336+
if mnt.ReadOnly() {
337+
if len(parentLayerPaths) == 0 {
338+
// rootfs.CreateDiff creates a new, empty View to diff against,
339+
// when diffing something with no parent.
340+
// This makes perfect sense for a walking Diff, but for WCOW,
341+
// we have to recognise this as "diff against nothing"
342+
return "", nil, nil
343+
}
344+
// Ignore the dummy sandbox.
345+
return parentLayerPaths[0], parentLayerPaths[1:], nil
346+
}
335347
return mnt.Source, parentLayerPaths, nil
336348
}
337349

@@ -346,8 +358,16 @@ func mountPairToLayerStack(lower, upper []mount.Mount) ([]string, error) {
346358
return nil, fmt.Errorf("Upper mount invalid: %w", err)
347359
}
348360

361+
lowerLayer, lowerParentLayerPaths, err := mountsToLayerAndParents(lower)
362+
if errdefs.IsNotImplemented(err) {
363+
// Upper was a windows-layer, lower is not. We can't handle that.
364+
return nil, fmt.Errorf("windowsDiff cannot diff a windows-layer against a non-windows-layer: %w", errdefs.ErrInvalidArgument)
365+
} else if err != nil {
366+
return nil, fmt.Errorf("Lower mount invalid: %w", err)
367+
}
368+
349369
// Trivial case, diff-against-nothing
350-
if len(lower) == 0 {
370+
if lowerLayer == "" {
351371
if len(upperParentLayerPaths) != 0 {
352372
return nil, fmt.Errorf("windowsDiff cannot diff a layer with parents against a null layer: %w", errdefs.ErrInvalidArgument)
353373
}
@@ -358,14 +378,6 @@ func mountPairToLayerStack(lower, upper []mount.Mount) ([]string, error) {
358378
return nil, fmt.Errorf("windowsDiff cannot diff a layer with no parents against another layer: %w", errdefs.ErrInvalidArgument)
359379
}
360380

361-
lowerLayer, lowerParentLayerPaths, err := mountsToLayerAndParents(lower)
362-
if errdefs.IsNotImplemented(err) {
363-
// Upper was a windows-layer, lower is not. We can't handle that.
364-
return nil, fmt.Errorf("windowsDiff cannot diff a windows-layer against a non-windows-layer: %w", errdefs.ErrInvalidArgument)
365-
} else if err != nil {
366-
return nil, fmt.Errorf("Lower mount invalid: %w", err)
367-
}
368-
369381
if upperParentLayerPaths[0] != lowerLayer {
370382
return nil, fmt.Errorf("windowsDiff cannot diff a layer against a layer other than its own parent: %w", errdefs.ErrInvalidArgument)
371383
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/containerd/btrfs/v2 v2.0.0
1313
github.com/containerd/cgroups/v3 v3.0.1
1414
github.com/containerd/console v1.0.3
15-
github.com/containerd/continuity v0.3.0
15+
github.com/containerd/continuity v0.4.1
1616
github.com/containerd/fifo v1.1.0
1717
github.com/containerd/go-cni v1.1.9
1818
github.com/containerd/go-runc v1.0.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR
233233
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
234234
github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
235235
github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk=
236-
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
237-
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
236+
github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5/EqGhU=
237+
github.com/containerd/continuity v0.4.1/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
238238
github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
239239
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
240240
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=

integration/client/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ go 1.19
44

55
require (
66
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // replaced; see replace rules for actual version used.
7+
github.com/Microsoft/go-winio v0.6.1 // indirect
78
github.com/Microsoft/hcsshim v0.10.0-rc.8
89
github.com/Microsoft/hcsshim/test v0.0.0-20210408205431-da33ecd607e1
910
github.com/containerd/cgroups/v3 v3.0.1
1011
github.com/containerd/containerd v1.7.0 // see replace; the actual version of containerd is replaced with the code at the root of this repository
11-
github.com/containerd/continuity v0.3.0
12+
github.com/containerd/continuity v0.4.1
1213
github.com/containerd/go-runc v1.0.0
1314
github.com/containerd/ttrpc v1.2.2
1415
github.com/containerd/typeurl/v2 v2.1.1
@@ -23,7 +24,6 @@ require (
2324

2425
require (
2526
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 // indirect
26-
github.com/Microsoft/go-winio v0.6.1 // indirect
2727
github.com/cilium/ebpf v0.9.1 // indirect
2828
github.com/containerd/cgroups v1.1.0 // indirect
2929
github.com/containerd/console v1.0.3 // indirect

integration/client/go.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,8 +654,9 @@ github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8a
654654
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
655655
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
656656
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
657-
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
658657
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
658+
github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5/EqGhU=
659+
github.com/containerd/continuity v0.4.1/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
659660
github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
660661
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
661662
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=

integration/client/snapshot_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package client
1818

1919
import (
2020
"context"
21-
"runtime"
2221
"testing"
2322

2423
. "github.com/containerd/containerd"
@@ -44,8 +43,6 @@ func TestSnapshotterClient(t *testing.T) {
4443
if testing.Short() {
4544
t.Skip()
4645
}
47-
if runtime.GOOS == "windows" {
48-
t.Skip("snapshots not yet supported on Windows")
49-
}
46+
5047
testsuite.SnapshotterSuite(t, DefaultSnapshotter, newSnapshotter)
5148
}

mount/mount.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ func UnmountMounts(mounts []Mount, target string, flags int) error {
6868
return nil
6969
}
7070

71+
// ReadOnly returns a boolean value indicating whether this mount has the "ro"
72+
// option set.
73+
func (m *Mount) ReadOnly() bool {
74+
for _, option := range m.Options {
75+
if option == "ro" {
76+
return true
77+
}
78+
}
79+
return false
80+
}
81+
7182
// Mount to the provided target path.
7283
func (m *Mount) Mount(target string) error {
7384
target, err := fs.RootPath(target, m.Target)

mount/mount_windows.go

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,29 @@
1717
package mount
1818

1919
import (
20+
"context"
2021
"encoding/json"
22+
"errors"
2123
"fmt"
2224
"os"
2325
"path/filepath"
2426
"strings"
2527

28+
"github.com/Microsoft/go-winio/pkg/bindfilter"
2629
"github.com/Microsoft/hcsshim"
30+
"github.com/containerd/containerd/log"
31+
"golang.org/x/sys/windows"
32+
)
33+
34+
const sourceStreamName = "containerd.io-source"
35+
36+
var (
37+
// ErrNotImplementOnWindows is returned when an action is not implemented for windows
38+
ErrNotImplementOnWindows = errors.New("not implemented under windows")
2739
)
2840

2941
// Mount to the provided target.
30-
func (m *Mount) mount(target string) error {
42+
func (m *Mount) mount(target string) (retErr error) {
3143
if m.Type != "windows-layer" {
3244
return fmt.Errorf("invalid windows mount type: '%s'", m.Type)
3345
}
@@ -43,25 +55,60 @@ func (m *Mount) mount(target string) error {
4355
HomeDir: home,
4456
}
4557

46-
if err = hcsshim.ActivateLayer(di, layerID); err != nil {
58+
if err := hcsshim.ActivateLayer(di, layerID); err != nil {
4759
return fmt.Errorf("failed to activate layer %s: %w", m.Source, err)
4860
}
61+
defer func() {
62+
if retErr != nil {
63+
if layerErr := hcsshim.DeactivateLayer(di, layerID); layerErr != nil {
64+
log.G(context.TODO()).WithError(layerErr).Error("failed to deactivate layer during mount failure cleanup")
65+
}
66+
}
67+
}()
4968

50-
if err = hcsshim.PrepareLayer(di, layerID, parentLayerPaths); err != nil {
69+
if err := hcsshim.PrepareLayer(di, layerID, parentLayerPaths); err != nil {
5170
return fmt.Errorf("failed to prepare layer %s: %w", m.Source, err)
5271
}
5372

54-
// We can link the layer mount path to the given target. It is an UNC path, and it needs
55-
// a trailing backslash.
56-
mountPath, err := hcsshim.GetLayerMountPath(di, layerID)
73+
defer func() {
74+
if retErr != nil {
75+
if layerErr := hcsshim.UnprepareLayer(di, layerID); layerErr != nil {
76+
log.G(context.TODO()).WithError(layerErr).Error("failed to unprepare layer during mount failure cleanup")
77+
}
78+
}
79+
}()
80+
81+
volume, err := hcsshim.GetLayerMountPath(di, layerID)
5782
if err != nil {
58-
return fmt.Errorf("failed to get layer mount path for %s: %w", m.Source, err)
83+
return fmt.Errorf("failed to get volume path for layer %s: %w", m.Source, err)
84+
}
85+
86+
if len(parentLayerPaths) == 0 {
87+
// this is a base layer. It gets mounted without going through WCIFS. We need to mount the Files
88+
// folder, not the actual source, or the client may inadvertently remove metadata files.
89+
volume = filepath.Join(volume, "Files")
90+
if _, err := os.Stat(volume); err != nil {
91+
return fmt.Errorf("no Files folder in layer %s", layerID)
92+
}
5993
}
60-
mountPath = mountPath + `\`
94+
if err := bindfilter.ApplyFileBinding(target, volume, m.ReadOnly()); err != nil {
95+
return fmt.Errorf("failed to set volume mount path for layer %s: %w", m.Source, err)
96+
}
97+
defer func() {
98+
if retErr != nil {
99+
if bindErr := bindfilter.RemoveFileBinding(target); bindErr != nil {
100+
log.G(context.TODO()).WithError(bindErr).Error("failed to remove binding during mount failure cleanup")
101+
}
102+
}
103+
}()
61104

62-
if err = os.Symlink(mountPath, target); err != nil {
63-
return fmt.Errorf("failed to link mount to target %s: %w", target, err)
105+
// Add an Alternate Data Stream to record the layer source.
106+
// See https://docs.microsoft.com/en-au/archive/blogs/askcore/alternate-data-streams-in-ntfs
107+
// for details on Alternate Data Streams.
108+
if err := os.WriteFile(filepath.Clean(target)+":"+sourceStreamName, []byte(m.Source), 0666); err != nil {
109+
return fmt.Errorf("failed to record source for layer %s: %w", m.Source, err)
64110
}
111+
65112
return nil
66113
}
67114

@@ -85,25 +132,55 @@ func (m *Mount) GetParentPaths() ([]string, error) {
85132

86133
// Unmount the mount at the provided path
87134
func Unmount(mount string, flags int) error {
88-
var (
89-
home, layerID = filepath.Split(mount)
90-
di = hcsshim.DriverInfo{
91-
HomeDir: home,
135+
mount = filepath.Clean(mount)
136+
adsFile := mount + ":" + sourceStreamName
137+
var layerPath string
138+
139+
if _, err := os.Lstat(adsFile); err == nil {
140+
layerPathb, err := os.ReadFile(mount + ":" + sourceStreamName)
141+
if err != nil {
142+
return fmt.Errorf("failed to retrieve source for layer %s: %w", mount, err)
92143
}
93-
)
94-
95-
if err := hcsshim.UnprepareLayer(di, layerID); err != nil {
96-
return fmt.Errorf("failed to unprepare layer %s: %w", mount, err)
144+
layerPath = string(layerPathb)
97145
}
98-
if err := hcsshim.DeactivateLayer(di, layerID); err != nil {
99-
return fmt.Errorf("failed to deactivate layer %s: %w", mount, err)
146+
147+
if err := bindfilter.RemoveFileBinding(mount); err != nil {
148+
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) || errors.Is(err, windows.ERROR_NOT_FOUND) {
149+
// not a mount point
150+
return nil
151+
}
152+
return fmt.Errorf("removing mount: %w", err)
100153
}
101154

155+
if layerPath != "" {
156+
var (
157+
home, layerID = filepath.Split(layerPath)
158+
di = hcsshim.DriverInfo{
159+
HomeDir: home,
160+
}
161+
)
162+
163+
if err := hcsshim.UnprepareLayer(di, layerID); err != nil {
164+
return fmt.Errorf("failed to unprepare layer %s: %w", mount, err)
165+
}
166+
167+
if err := hcsshim.DeactivateLayer(di, layerID); err != nil {
168+
return fmt.Errorf("failed to deactivate layer %s: %w", mount, err)
169+
}
170+
}
102171
return nil
103172
}
104173

105174
// UnmountAll unmounts from the provided path
106175
func UnmountAll(mount string, flags int) error {
176+
if mount == "" {
177+
// This isn't an error, per the EINVAL handling in the Linux version
178+
return nil
179+
}
180+
if _, err := os.Stat(mount); os.IsNotExist(err) {
181+
return nil
182+
}
183+
107184
return Unmount(mount, flags)
108185
}
109186

0 commit comments

Comments
 (0)