Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 43 additions & 37 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -1345,15 +1345,15 @@ func resultKey(node *noderesolver.ResolvedNode, name string) string {
// and creates a separate session that will be used by all detected requests.
func detectSharedMounts(ctx context.Context, reqs map[string][]*reqForNode) (_ map[string][]*session.Session, err error) {
type fsTracker struct {
fs fsutil.FS
fs *fsMount
so []*client.SolveOpt
}
type fsKey struct {
name string
dir string
}

m := map[string]map[fsKey]*fsTracker{}
m := map[string]map[fsKey][]*fsTracker{}
for _, reqs := range reqs {
for _, req := range reqs {
nodeName := req.Node().Name
Expand All @@ -1362,26 +1362,30 @@ func detectSharedMounts(ctx context.Context, reqs map[string][]*reqForNode) (_ m
continue
}
if _, ok := m[nodeName]; !ok {
m[nodeName] = map[fsKey]*fsTracker{}
m[nodeName] = map[fsKey][]*fsTracker{}
}
fsMap := m[nodeName]
for name, m := range req.so.LocalMounts {
fs, ok := m.(*fsMount)
for name, mount := range req.so.LocalMounts {
fs, ok := mount.(*fsMount)
if !ok {
continue
}
key := fsKey{name: name, dir: fs.dir}
if _, ok := fsMap[key]; !ok {
fsMap[key] = &fsTracker{fs: fs.FS}
idx := slices.IndexFunc(fsMap[key], func(t *fsTracker) bool {
return slices.Equal(t.fs.patterns, fs.patterns)
})
if idx == -1 {
fsMap[key] = append(fsMap[key], &fsTracker{fs: fs})
idx = len(fsMap[key]) - 1
}
fsMap[key].so = append(fsMap[key].so, req.so)
fsMap[key][idx].so = append(fsMap[key][idx].so, req.so)
}
}
}

type sharedSession struct {
*session.Session
fsMap map[string]fsutil.FS
fsMap map[string]*fsMount
}

sessionMap := map[string][]*sharedSession{}
Expand All @@ -1397,38 +1401,40 @@ func detectSharedMounts(ctx context.Context, reqs map[string][]*reqForNode) (_ m
}()

for node, fsMap := range m {
for key, fs := range fsMap {
if len(fs.so) <= 1 {
continue
}
for key, trackers := range fsMap {
for _, fs := range trackers {
if len(fs.so) <= 1 {
continue
}

sessions := sessionMap[node]
sessions := sessionMap[node]

// find session that doesn't have the fs name reserved
idx := slices.IndexFunc(sessions, func(s *sharedSession) bool {
_, ok := s.fsMap[key.name]
return !ok
})
// find session that doesn't have the fs name reserved
idx := slices.IndexFunc(sessions, func(s *sharedSession) bool {
_, ok := s.fsMap[key.name]
return !ok
})

var ss *sharedSession
if idx == -1 {
s, err := session.NewSession(ctx, fs.so[0].SharedKey)
if err != nil {
return nil, err
var ss *sharedSession
if idx == -1 {
s, err := session.NewSession(ctx, fs.so[0].SharedKey)
if err != nil {
return nil, err
}
ss = &sharedSession{Session: s, fsMap: map[string]*fsMount{}}
sessions = append(sessions, ss)
sessionMap[node] = sessions
} else {
ss = sessions[idx]
}
ss = &sharedSession{Session: s, fsMap: map[string]fsutil.FS{}}
sessions = append(sessions, ss)
sessionMap[node] = sessions
} else {
ss = sessions[idx]
}

ss.fsMap[key.name] = fs.fs
for _, so := range fs.so {
if so.FrontendAttrs == nil {
so.FrontendAttrs = map[string]string{}
ss.fsMap[key.name] = fs.fs
for _, so := range fs.so {
if so.FrontendAttrs == nil {
so.FrontendAttrs = map[string]string{}
}
so.FrontendAttrs["local-sessionid:"+key.name] = ss.ID()
}
so.FrontendAttrs["local-sessionid:"+key.name] = ss.ID()
}
}
}
Expand All @@ -1447,8 +1453,8 @@ func detectSharedMounts(ctx context.Context, reqs map[string][]*reqForNode) (_ m
arr = append(arr, s.Session)

src := make(filesync.StaticDirSource, len(s.fsMap))
for name, fs := range s.fsMap {
fs, err := fsutil.NewFilterFS(fs, &fsutil.FilterOpt{
for name, mount := range s.fsMap {
fs, err := fsutil.NewFilterFS(mount.FS, &fsutil.FilterOpt{
Map: resetUIDAndGID,
})
if err != nil {
Expand Down
48 changes: 43 additions & 5 deletions build/opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import (
"github.com/moby/buildkit/util/archiveutil"
"github.com/moby/buildkit/util/entitlements"
"github.com/moby/buildkit/util/gitutil"
"github.com/moby/patternmatcher/ignorefile"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
Expand Down Expand Up @@ -577,7 +578,7 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
so.FrontendAttrs["label:"+k] = v
}

releaseLoad, err := loadInputs(ctx, nodeDriver, &opt.Inputs, pw, &so)
releaseLoad, err := loadInputs(ctx, nodeDriver, &opt.Inputs, cfg, pw, &so)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -897,7 +898,7 @@ func policyEnvFilename(inp Inputs) string {
return "Dockerfile"
}

func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw progress.Writer, target *client.SolveOpt) (func(), error) {
func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, cfg *confutil.Config, pw progress.Writer, target *client.SolveOpt) (func(), error) {
if inp.ContextPath == "" {
return nil, errors.New("please specify build context (e.g. \".\" for the current directory)")
}
Expand Down Expand Up @@ -959,7 +960,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
}
}
case osutil.IsLocalDir(inp.ContextPath):
if err := setLocalMount("context", inp.ContextPath, target); err != nil {
if err := setLocalContextMount("context", inp.ContextPath, cfg, target); err != nil {
return nil, err
}
contextDir = inp.ContextPath
Expand Down Expand Up @@ -1139,7 +1140,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
if k == "context" || k == "dockerfile" {
localName = "_" + k // underscore to avoid collisions
}
if err := setLocalMount(localName, v.Path, target); err != nil {
if err := setLocalContextMount(localName, v.Path, cfg, target); err != nil {
return nil, err
}
target.FrontendAttrs["context:"+k] = "local:" + localName
Expand Down Expand Up @@ -1231,6 +1232,42 @@ func setLocalMount(name, dir string, so *client.SolveOpt) error {
return nil
}

func setLocalContextMount(name, dir string, cfg *confutil.Config, so *client.SolveOpt) error {
if err := setLocalMount(name, dir, so); err != nil {
return err
}

filename := filepath.Join(cfg.Dir(), ".dockerignore")
f, err := os.Open(filename)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return errors.Wrapf(err, "failed to open global ignore file %s", filename)
}
Comment on lines +1242 to +1247

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        return nil
    }
    return errors.Wrapf(err, "...", ...)
}

You could also just set err = nil and call errors.Wrapf either way since that function will return nil if it is passed a nil error.

defer f.Close()

patterns, err := ignorefile.ReadAll(f)
if err != nil {
return errors.Wrapf(err, "failed to read global ignore file %s", filename)
}
if len(patterns) == 0 {
return nil
}
Comment on lines +1250 to +1256

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can take advantage of that here by doing err != nil || len(patterns) == 0.

// BuildKit reads the context's ignore file through this mount before
// applying its repository-specific rules.
mount := so.LocalMounts[name].(*fsMount)
filtered, err := fsutil.NewFilterFS(mount.FS, &fsutil.FilterOpt{
ExcludePatterns: append(slices.Clone(patterns), "!.dockerignore"),
})
if err != nil {
return errors.Wrapf(err, "failed to parse global ignore file %s", filename)
}
mount.FS = filtered
mount.patterns = patterns
return nil
}

func createTempDockerfile(r io.Reader, multiReader *SyncMultiReader) (string, error) {
dir, err := os.MkdirTemp("", "dockerfile")
if err != nil {
Expand Down Expand Up @@ -1341,7 +1378,8 @@ func handleLowercaseDockerfile(dir, p string) string {

type fsMount struct {
fsutil.FS
dir string
dir string
patterns []string
}

var _ fsutil.FS = &fsMount{}
Expand Down
91 changes: 90 additions & 1 deletion build/opt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -32,6 +34,7 @@ import (
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tonistiigi/fsutil"
)

type exporterTestDriver struct {
Expand Down Expand Up @@ -204,6 +207,92 @@ func TestDriverFeatureFailurePreservesProvenance(t *testing.T) {
require.Equal(t, 2, calls)
}

func TestGlobalDockerignore(t *testing.T) {
contextDir := t.TempDir()
configDir := t.TempDir()
for name, content := range map[string]string{
"keep.txt": "keep",
"keep.bin": "keep",
"drop.txt": "drop",
"secret.pem": "secret",
".dockerignore": "*.log\n",
"debug.log": "log",
".idea/state": "editor",
".idea/keep": "editor",
} {
path := filepath.Join(contextDir, name)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}
require.NoError(t, os.WriteFile(filepath.Join(configDir, ".dockerignore"), []byte(".idea/*\n!.idea/keep\n.dockerignore\n*.txt\n*.pem\n!secret.pem\n"), 0o644))

so := &client.SolveOpt{}
require.NoError(t, setLocalContextMount("context", contextDir, confutil.NewConfig(nil, confutil.WithDir(configDir)), so))
// The global filter keeps the ignore file available for the frontend.
f, err := so.LocalMounts["context"].Open(".dockerignore")
require.NoError(t, err)
require.NoError(t, f.Close())
opt := &fsutil.FilterOpt{ExcludePatterns: []string{"!keep.txt", "secret.pem", ".dockerignore"}}
filtered, err := fsutil.NewFilterFS(so.LocalMounts["context"], opt)
require.NoError(t, err)
for _, name := range []string{"keep.bin", "debug.log", ".idea/keep"} {
f, err := filtered.Open(name)
require.NoError(t, err, name)
_, err = io.ReadAll(f)
require.NoError(t, err)
require.NoError(t, f.Close())
}
_, err = filtered.Open(".idea/state")
require.ErrorIs(t, err, os.ErrNotExist)
_, err = filtered.Open("drop.txt")
require.ErrorIs(t, err, os.ErrNotExist)
_, err = filtered.Open("keep.txt")
require.ErrorIs(t, err, os.ErrNotExist)
_, err = filtered.Open("secret.pem")
require.ErrorIs(t, err, os.ErrNotExist)
_, err = filtered.Open(".dockerignore")
require.ErrorIs(t, err, os.ErrNotExist)

// A missing global file leaves local mounts unchanged.
require.NoError(t, setLocalContextMount("other", contextDir, confutil.NewConfig(nil, confutil.WithDir(t.TempDir())), so))
f, err = so.LocalMounts["other"].Open(".idea/state")
require.NoError(t, err)
require.NoError(t, f.Close())
}

func TestGlobalDockerignoreInvalidPattern(t *testing.T) {
configDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(configDir, ".dockerignore"), []byte("[\n"), 0o644))
err := setLocalContextMount("context", t.TempDir(), confutil.NewConfig(nil, confutil.WithDir(configDir)), &client.SolveOpt{})
require.ErrorContains(t, err, "failed to parse global ignore file")
}

func TestLoadInputsGlobalDockerignore(t *testing.T) {
contextDir := t.TempDir()
namedDir := t.TempDir()
configDir := t.TempDir()
for _, dir := range []string{contextDir, namedDir} {
require.NoError(t, os.WriteFile(filepath.Join(dir, "local.txt"), []byte("local"), 0o644))
}
require.NoError(t, os.WriteFile(filepath.Join(contextDir, "Containerfile"), []byte("FROM scratch\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(configDir, ".dockerignore"), []byte("local.txt\n"), 0o644))

so := &client.SolveOpt{FrontendAttrs: map[string]string{}}
inp := &Inputs{ContextPath: contextDir, DockerfilePath: filepath.Join(contextDir, "Containerfile"), NamedContexts: map[string]NamedContext{
"source": {Path: namedDir},
}}
release, err := loadInputs(context.Background(), nil, inp, confutil.NewConfig(nil, confutil.WithDir(configDir)), testProgressWriter{}, so)
require.NoError(t, err)
t.Cleanup(release)
for _, name := range []string{"context", "source"} {
_, err = so.LocalMounts[name].Open("local.txt")
require.ErrorIs(t, err, os.ErrNotExist)
}
f, err := so.LocalMounts["dockerfile"].Open("local.txt")
require.NoError(t, err)
require.NoError(t, f.Close())
}

func TestCacheOptions_DerivedVars(t *testing.T) {
t.Setenv("ACTIONS_RUNTIME_TOKEN", "sensitive_token")
t.Setenv("ACTIONS_CACHE_URL", "https://cache.github.com")
Expand Down Expand Up @@ -467,7 +556,7 @@ func TestLoadInputsOCILayoutNamedContext(t *testing.T) {
},
}

release, err := loadInputs(context.Background(), nil, inp, testProgressWriter{}, target)
release, err := loadInputs(context.Background(), nil, inp, nil, testProgressWriter{}, target)
require.NoError(t, err)
require.NotNil(t, release)
t.Cleanup(release)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/moby/buildkit v0.34.0-rc1
github.com/moby/moby/api v1.56.0
github.com/moby/moby/client v0.6.0
github.com/moby/patternmatcher v0.6.1
github.com/moby/policy-helpers v0.0.0-20260901142052-72f704e6cdb6
github.com/moby/sys/atomicwriter v0.1.0
github.com/moby/sys/mountinfo v0.7.2
Expand Down Expand Up @@ -170,7 +171,6 @@ require (
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/spdystream v0.5.1 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
Expand Down
Loading
Loading