diff --git a/frontend/dockerui/config.go b/frontend/dockerui/config.go index 27ccc6f16b41..556a85f0cb36 100644 --- a/frontend/dockerui/config.go +++ b/frontend/dockerui/config.go @@ -435,22 +435,31 @@ func (bc *Client) MainContext(ctx context.Context, opts ...llb.LocalOption) (*ll } sessionID := bc.bopts.SessionID - if v, ok := bc.localsSessionIDs[bctx.contextLocalName]; ok { - sessionID = v + sharedSessionID, sharedSession := bc.localsSessionIDs[bctx.contextLocalName] + if sharedSession { + sessionID = sharedSessionID } - opts = append([]llb.LocalOption{ - llb.SessionID(sessionID), - llb.ExcludePatterns(excludes), - llb.SharedKeyHint(bctx.contextLocalName), - WithInternalName("load build context"), - }, opts...) - + opts = mainContextLocalOpts(bctx.contextLocalName, sessionID, excludes, opts, sharedSession) st := llb.Local(bctx.contextLocalName, opts...) return &st, nil } +func mainContextLocalOpts(name, sessionID string, excludes []string, callerOpts []llb.LocalOption, sharedSession bool) []llb.LocalOption { + opts := []llb.LocalOption{ + llb.SessionID(sessionID), + llb.ExcludePatterns(excludes), + llb.SharedKeyHint(name), + WithInternalName("load build context"), + } + opts = append(opts, callerOpts...) + if sharedSession { + opts = append(opts, llb.FollowPaths(nil)) + } + return opts +} + func (bc *Client) NamedContext(name string, opt ContextOpt) (*NamedContext, error) { named, err := reference.ParseNormalizedNamed(name) if err != nil { diff --git a/frontend/dockerui/main_context_test.go b/frontend/dockerui/main_context_test.go new file mode 100644 index 000000000000..9befa2b2274f --- /dev/null +++ b/frontend/dockerui/main_context_test.go @@ -0,0 +1,65 @@ +package dockerui + +import ( + "context" + "testing" + + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/solver/pb" + "github.com/stretchr/testify/require" +) + +func findLocalAttr(t *testing.T, def *llb.Definition, attr string) string { + t.Helper() + for _, dt := range def.Def { + var op pb.Op + require.NoError(t, op.Unmarshal(dt)) + src := op.GetSource() + if src == nil { + continue + } + if v, ok := src.Attrs[attr]; ok { + return v + } + } + return "" +} + +func TestMainContextSharedSessionDropsFollowPaths(t *testing.T) { + ctx := context.Background() + callerOpts := []llb.LocalOption{ + llb.FollowPaths([]string{"app/cfg"}), + } + + t.Run("non-shared session preserves caller FollowPaths", func(t *testing.T) { + opts := mainContextLocalOpts("context", "sess-A", nil, callerOpts, false) + st := llb.Local("context", opts...) + def, err := st.Marshal(ctx) + require.NoError(t, err) + require.NotEmpty(t, findLocalAttr(t, def, pb.AttrFollowPaths)) + }) + + t.Run("shared session clears caller FollowPaths", func(t *testing.T) { + opts := mainContextLocalOpts("context", "sess-shared", nil, callerOpts, true) + st := llb.Local("context", opts...) + def, err := st.Marshal(ctx) + require.NoError(t, err) + require.Empty(t, findLocalAttr(t, def, pb.AttrFollowPaths)) + }) + + t.Run("shared session makes divergent caller FollowPaths identical", func(t *testing.T) { + optsA := mainContextLocalOpts("context", "sess-shared", nil, + []llb.LocalOption{llb.FollowPaths([]string{"app/cfg"})}, true) + optsB := mainContextLocalOpts("context", "sess-shared", nil, + []llb.LocalOption{llb.FollowPaths([]string{"tools/cfg"})}, true) + + stA := llb.Local("context", optsA...) + stB := llb.Local("context", optsB...) + defA, err := stA.Marshal(ctx) + require.NoError(t, err) + defB, err := stB.Marshal(ctx) + require.NoError(t, err) + + require.Equal(t, defA.Def, defB.Def) + }) +}