Skip to content

Commit daa3a76

Browse files
laurazardzouyee
andcommitted
Add WithReadonlyTempMount to create readonly temporary mounts
This is necessary so we can mount snapshots more than once with overlayfs, otherwise mounts enter an unknown state. related: moby/buildkit#1100 Signed-off-by: Laura Brehm <[email protected]> Co-authored-by: Zou Nengren <[email protected]>
1 parent 1fbd703 commit daa3a76

File tree

4 files changed

+202
-1
lines changed

4 files changed

+202
-1
lines changed

diff/walking/differ.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (s *walkingDiff) Compare(ctx context.Context, lower, upper []mount.Mount, o
9696

9797
var ocidesc ocispec.Descriptor
9898
if err := mount.WithTempMount(ctx, lower, func(lowerRoot string) error {
99-
return mount.WithTempMount(ctx, upper, func(upperRoot string) error {
99+
return mount.WithReadonlyTempMount(ctx, upper, func(upperRoot string) error {
100100
var newReference bool
101101
if config.Reference == "" {
102102
newReference = true

mount/mount.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package mount
1818

1919
import (
2020
"fmt"
21+
"strings"
2122

2223
"github.com/containerd/continuity/fs"
2324
)
@@ -75,3 +76,46 @@ func (m *Mount) Mount(target string) error {
7576
}
7677
return m.mount(target)
7778
}
79+
80+
// readonlyMounts modifies the received mount options
81+
// to make them readonly
82+
func readonlyMounts(mounts []Mount) []Mount {
83+
for i, m := range mounts {
84+
if m.Type == "overlay" {
85+
mounts[i].Options = readonlyOverlay(m.Options)
86+
continue
87+
}
88+
opts := make([]string, 0, len(m.Options))
89+
for _, opt := range m.Options {
90+
if opt != "rw" && opt != "ro" { // skip `ro` too so we don't append it twice
91+
opts = append(opts, opt)
92+
}
93+
}
94+
opts = append(opts, "ro")
95+
mounts[i].Options = opts
96+
}
97+
return mounts
98+
}
99+
100+
// readonlyOverlay takes mount options for overlay mounts and makes them readonly by
101+
// removing workdir and upperdir (and appending the upperdir layer to lowerdir) - see:
102+
// https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#multiple-lower-layers
103+
func readonlyOverlay(opt []string) []string {
104+
out := make([]string, 0, len(opt))
105+
upper := ""
106+
for _, o := range opt {
107+
if strings.HasPrefix(o, "upperdir=") {
108+
upper = strings.TrimPrefix(o, "upperdir=")
109+
} else if !strings.HasPrefix(o, "workdir=") {
110+
out = append(out, o)
111+
}
112+
}
113+
if upper != "" {
114+
for i, o := range out {
115+
if strings.HasPrefix(o, "lowerdir=") {
116+
out[i] = "lowerdir=" + upper + ":" + strings.TrimPrefix(o, "lowerdir=")
117+
}
118+
}
119+
}
120+
return out
121+
}

mount/mount_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mount
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
23+
// required for `-test.root` flag not to fail
24+
_ "github.com/containerd/continuity/testutil"
25+
)
26+
27+
func TestReadonlyMounts(t *testing.T) {
28+
testCases := []struct {
29+
desc string
30+
input []Mount
31+
expected []Mount
32+
}{
33+
{
34+
desc: "empty slice",
35+
input: []Mount{},
36+
expected: []Mount{},
37+
},
38+
{
39+
desc: "removes `upperdir` and `workdir` from overlay mounts, appends upper layer to lower",
40+
input: []Mount{
41+
{
42+
Type: "overlay",
43+
Source: "overlay",
44+
Options: []string{
45+
"index=off",
46+
"workdir=/path/to/snapshots/4/work",
47+
"upperdir=/path/to/snapshots/4/fs",
48+
"lowerdir=/path/to/snapshots/1/fs",
49+
},
50+
},
51+
{
52+
Type: "overlay",
53+
Source: "overlay",
54+
Options: []string{
55+
"index=on",
56+
"lowerdir=/another/path/to/snapshots/2/fs",
57+
},
58+
},
59+
},
60+
expected: []Mount{
61+
{
62+
Type: "overlay",
63+
Source: "overlay",
64+
Options: []string{
65+
"index=off",
66+
"lowerdir=/path/to/snapshots/4/fs:/path/to/snapshots/1/fs",
67+
},
68+
},
69+
{
70+
Type: "overlay",
71+
Source: "overlay",
72+
Options: []string{
73+
"index=on",
74+
"lowerdir=/another/path/to/snapshots/2/fs",
75+
},
76+
},
77+
},
78+
},
79+
{
80+
desc: "removes `rw` and appends `ro` (once) to other mount types",
81+
input: []Mount{
82+
{
83+
Type: "mount-without-rw",
84+
Source: "",
85+
Options: []string{
86+
"index=off",
87+
"workdir=/path/to/other/snapshots/work",
88+
"upperdir=/path/to/other/snapshots/2",
89+
"lowerdir=/path/to/other/snapshots/1",
90+
},
91+
},
92+
{
93+
Type: "mount-with-rw",
94+
Source: "",
95+
Options: []string{
96+
"an-option=a-value",
97+
"another_opt=/another/value",
98+
"rw",
99+
},
100+
},
101+
{
102+
Type: "mount-with-ro",
103+
Source: "",
104+
Options: []string{
105+
"an-option=a-value",
106+
"another_opt=/another/value",
107+
"ro",
108+
},
109+
},
110+
},
111+
expected: []Mount{
112+
{
113+
Type: "mount-without-rw",
114+
Source: "",
115+
Options: []string{
116+
"index=off",
117+
"workdir=/path/to/other/snapshots/work",
118+
"upperdir=/path/to/other/snapshots/2",
119+
"lowerdir=/path/to/other/snapshots/1",
120+
"ro",
121+
},
122+
},
123+
{
124+
Type: "mount-with-rw",
125+
Source: "",
126+
Options: []string{
127+
"an-option=a-value",
128+
"another_opt=/another/value",
129+
"ro",
130+
},
131+
},
132+
{
133+
Type: "mount-with-ro",
134+
Source: "",
135+
Options: []string{
136+
"an-option=a-value",
137+
"another_opt=/another/value",
138+
"ro",
139+
},
140+
},
141+
},
142+
},
143+
}
144+
145+
for _, tc := range testCases {
146+
if !reflect.DeepEqual(readonlyMounts(tc.input), tc.expected) {
147+
t.Fatalf("incorrectly modified mounts: %s", tc.desc)
148+
}
149+
}
150+
}

mount/temp.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ func WithTempMount(ctx context.Context, mounts []Mount, f func(root string) erro
6767
return nil
6868
}
6969

70+
// WithReadonlyTempMount mounts the provided mounts to a temp dir as readonly,
71+
// and pass the temp dir to f. The mounts are valid during the call to the f.
72+
// Finally we will unmount and remove the temp dir regardless of the result of f.
73+
func WithReadonlyTempMount(ctx context.Context, mounts []Mount, f func(root string) error) (err error) {
74+
return WithTempMount(ctx, readonlyMounts(mounts), f)
75+
}
76+
7077
func getTempDir() string {
7178
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
7279
return xdg

0 commit comments

Comments
 (0)