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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,10 @@ func detectSharedMounts(ctx context.Context, reqs map[string][]*reqForNode) (_ m
m := map[string]map[fsKey]*fsTracker{}
for _, reqs := range reqs {
for _, req := range reqs {
// Shared providers cannot apply each solve's filter hook.
if req.so.LocalFilterOpt != nil {
continue
}
nodeName := req.Node().Name
// skip shared-session optimisation: targets may connect to different replicas.
if req.Node().Driver != nil && req.Node().Driver.RequiresUncachedClient() {
Expand Down
54 changes: 50 additions & 4 deletions build/opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import (
"github.com/moby/buildkit/util/archiveutil"
"github.com/moby/buildkit/util/entitlements"
"github.com/moby/buildkit/util/gitutil"
"github.com/moby/patternmatcher"
"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 @@ -554,7 +556,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 @@ -829,7 +831,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 @@ -891,7 +893,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 @@ -1071,7 +1073,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 @@ -1163,6 +1165,50 @@ 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)
}
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
}
// BuildKit reads the context's ignore file through this mount before
// applying its repository-specific rules.
patterns = append(patterns, "!.dockerignore")

if _, err := patternmatcher.New(patterns); err != nil {
return errors.Wrapf(err, "failed to parse global ignore file %s", filename)
}
previous := so.LocalFilterOpt
so.LocalFilterOpt = func(dirName string, opt *fsutil.FilterOpt) error {
if previous != nil {
if err := previous(dirName, opt); err != nil {
return err
}
}
if dirName == name {
opt.ExcludePatterns = append(slices.Clone(patterns), opt.ExcludePatterns...)
}
return nil
}
return nil
}

func createTempDockerfile(r io.Reader, multiReader *SyncMultiReader) (string, error) {
dir, err := os.MkdirTemp("", "dockerfile")
if err != nil {
Expand Down
92 changes: 91 additions & 1 deletion build/opt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package build

import (
"context"
"io"
"os"
"path/filepath"
"sync"
"testing"

"github.com/docker/buildx/policy"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/ocilayout"
"github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
Expand All @@ -19,8 +23,94 @@ import (
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tonistiigi/fsutil"
)

func TestGlobalDockerignore(t *testing.T) {
contextDir := t.TempDir()
configDir := t.TempDir()
for name, content := range map[string]string{
"keep.txt": "keep",
"drop.txt": "drop",
".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"), 0o644))

so := &client.SolveOpt{}
require.NoError(t, setLocalContextMount("context", contextDir, confutil.NewConfig(nil, confutil.WithDir(configDir)), so))
opt := &fsutil.FilterOpt{ExcludePatterns: []string{"!keep.txt"}}
require.NoError(t, so.LocalFilterOpt("context", opt))
filtered, err := fsutil.NewFilterFS(so.LocalMounts["context"], opt)
require.NoError(t, err)
for _, name := range []string{"keep.txt", ".dockerignore", "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)

// A missing global file leaves local mounts unchanged.
require.NoError(t, setLocalContextMount("other", contextDir, confutil.NewConfig(nil, confutil.WithDir(t.TempDir())), so))
otherOpt := &fsutil.FilterOpt{}
require.NoError(t, so.LocalFilterOpt("other", otherOpt))
require.Empty(t, otherOpt.ExcludePatterns)
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"} {
opt := &fsutil.FilterOpt{}
require.NoError(t, so.LocalFilterOpt(name, opt))
filtered, err := fsutil.NewFilterFS(so.LocalMounts[name], opt)
require.NoError(t, err)
_, err = filtered.Open("local.txt")
require.ErrorIs(t, err, os.ErrNotExist)
}
dockerfileOpt := &fsutil.FilterOpt{}
require.NoError(t, so.LocalFilterOpt("dockerfile", dockerfileOpt))
require.Empty(t, dockerfileOpt.ExcludePatterns)
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 @@ -284,7 +374,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
Loading
Loading