Skip to content

Commit 4438f4f

Browse files
authored
Merge pull request moby#4585 from cpuguy83/exclude_cachemount_id_from_cachemap
Do not include a cache mount's ID in the ExecOp's cachemap
2 parents 23b9dd8 + 584ec40 commit 4438f4f

File tree

2 files changed

+165
-1
lines changed

2 files changed

+165
-1
lines changed

solver/llbsolver/ops/exec.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,55 @@ func cloneExecOp(old *pb.ExecOp) pb.ExecOp {
9090
n.Mounts = nil
9191
for i := range old.Mounts {
9292
m := *old.Mounts[i]
93+
94+
if m.CacheOpt != nil {
95+
co := *m.CacheOpt
96+
m.CacheOpt = &co
97+
}
98+
9399
n.Mounts = append(n.Mounts, &m)
94100
}
95101
return n
96102
}
97103

104+
func checkShouldClearCacheOpts(m *pb.Mount) bool {
105+
if m.CacheOpt == nil {
106+
return false
107+
}
108+
109+
// This is a dockerfile default cache mount.
110+
// We are treating this as a special case so we don't cause a cache miss unintentionally.
111+
if m.CacheOpt.ID == m.Dest && m.CacheOpt.Sharing == 0 {
112+
return false
113+
}
114+
115+
// Check the case where a dockerfile cache-namespace may be used.
116+
// This would be `<namespace>/<dest>`
117+
_, trimmed, ok := strings.Cut(m.CacheOpt.ID, "/")
118+
if ok && trimmed == m.Dest && m.CacheOpt.Sharing == 0 {
119+
return false
120+
}
121+
122+
return true
123+
}
124+
98125
func (e *ExecOp) CacheMap(ctx context.Context, g session.Group, index int) (*solver.CacheMap, bool, error) {
99126
op := cloneExecOp(e.op)
127+
100128
for i := range op.Meta.ExtraHosts {
101129
h := op.Meta.ExtraHosts[i]
102130
h.IP = ""
103131
op.Meta.ExtraHosts[i] = h
104132
}
133+
105134
for i := range op.Mounts {
106-
op.Mounts[i].Selector = ""
135+
m := op.Mounts[i]
136+
m.Selector = ""
137+
138+
if checkShouldClearCacheOpts(m) {
139+
m.CacheOpt.ID = ""
140+
m.CacheOpt.Sharing = 0
141+
}
107142
}
108143
op.Meta.ProxyEnv = nil
109144

solver/llbsolver/ops/exec_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package ops
22

33
import (
4+
"context"
45
"testing"
56

7+
"github.com/moby/buildkit/identity"
8+
"github.com/moby/buildkit/session"
9+
"github.com/moby/buildkit/solver/pb"
610
"github.com/stretchr/testify/require"
711
)
812

@@ -28,3 +32,128 @@ func TestDedupePaths(t *testing.T) {
2832
res = dedupePaths([]string{"/", "/foo"})
2933
require.Equal(t, []string{"/"}, res)
3034
}
35+
36+
func TestExecOpCacheMap(t *testing.T) {
37+
type testCase struct {
38+
name string
39+
op1, op2 *ExecOp
40+
xMatch bool
41+
}
42+
43+
newExecOp := func(opts ...func(*ExecOp)) *ExecOp {
44+
op := &ExecOp{op: &pb.ExecOp{Meta: &pb.Meta{}}}
45+
for _, opt := range opts {
46+
opt(op)
47+
}
48+
return op
49+
}
50+
51+
withNewMount := func(p string, cache *pb.CacheOpt) func(*ExecOp) {
52+
return func(op *ExecOp) {
53+
m := &pb.Mount{
54+
Dest: p,
55+
Input: pb.InputIndex(op.numInputs),
56+
// Generate a new selector for each mount since this should not effect the cache key.
57+
// This helps exercise that code path.
58+
Selector: identity.NewID(),
59+
}
60+
if cache != nil {
61+
m.CacheOpt = cache
62+
m.MountType = pb.MountType_CACHE
63+
}
64+
op.op.Mounts = append(op.op.Mounts, m)
65+
op.numInputs++
66+
}
67+
}
68+
69+
withEmptyMounts := func(op *ExecOp) {
70+
op.op.Mounts = []*pb.Mount{}
71+
}
72+
73+
testCases := []testCase{
74+
{name: "empty", op1: newExecOp(), op2: newExecOp(), xMatch: true},
75+
{
76+
name: "empty vs with non-nil but empty mounts should match",
77+
op1: newExecOp(),
78+
op2: newExecOp(withEmptyMounts),
79+
xMatch: true,
80+
},
81+
{
82+
name: "both non-nil but empty mounts should match",
83+
op1: newExecOp(withEmptyMounts),
84+
op2: newExecOp(withEmptyMounts),
85+
xMatch: true,
86+
},
87+
{
88+
name: "non-nil but empty mounts vs with mounts should not match",
89+
op1: newExecOp(withEmptyMounts),
90+
op2: newExecOp(withNewMount("/foo", nil)),
91+
xMatch: false,
92+
},
93+
{
94+
name: "mounts to different paths should not match",
95+
op1: newExecOp(withNewMount("/foo", nil)),
96+
op2: newExecOp(withNewMount("/bar", nil)),
97+
xMatch: false,
98+
},
99+
{
100+
name: "mounts to same path should match",
101+
op1: newExecOp(withNewMount("/foo", nil)),
102+
op2: newExecOp(withNewMount("/foo", nil)),
103+
xMatch: true,
104+
},
105+
{
106+
name: "cache mount should not match non-cache mount at same path",
107+
op1: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "someID"})),
108+
op2: newExecOp(withNewMount("/foo", nil)),
109+
xMatch: false,
110+
},
111+
{
112+
name: "different cache id's at the same path should match",
113+
op1: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "someID"})),
114+
op2: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "someOtherID"})),
115+
xMatch: true,
116+
},
117+
{
118+
// This is a special case for default dockerfile cache mounts for backwards compatibility.
119+
name: "default dockerfile cache mount should not match the same cache mount but with different sharing",
120+
op1: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "/foo"})),
121+
op2: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "/foo", Sharing: pb.CacheSharingOpt_LOCKED})),
122+
xMatch: false,
123+
},
124+
{
125+
name: "cache mounts with the same ID but different sharing options should match",
126+
op1: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "someID", Sharing: 0})),
127+
op2: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "someID", Sharing: 1})),
128+
xMatch: true,
129+
},
130+
{
131+
name: "cache mounts with different IDs and different sharing should match at the same path",
132+
op1: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "someID", Sharing: 0})),
133+
op2: newExecOp(withNewMount("/foo", &pb.CacheOpt{ID: "someOtherID", Sharing: 1})),
134+
xMatch: true,
135+
},
136+
}
137+
138+
ctx := context.Background()
139+
for _, tc := range testCases {
140+
tc := tc
141+
t.Run(tc.name, func(t *testing.T) {
142+
t.Parallel()
143+
144+
m1, ok, err := tc.op1.CacheMap(ctx, session.NewGroup(t.Name()), 1)
145+
require.NoError(t, err)
146+
require.True(t, ok)
147+
148+
m2, ok, err := tc.op2.CacheMap(ctx, session.NewGroup(t.Name()), 1)
149+
require.NoError(t, err)
150+
require.True(t, ok)
151+
152+
if tc.xMatch {
153+
require.Equal(t, m1.Digest, m2.Digest, "\n\nm1: %+v\nm2: %+v", m1, m2)
154+
} else {
155+
require.NotEqual(t, m1.Digest, m2.Digest, "\n\nm1: %+v\nm2: %+v", m1, m2)
156+
}
157+
})
158+
}
159+
}

0 commit comments

Comments
 (0)