Skip to content

Commit 668daf5

Browse files
committed
build: apply global .dockerignore during context sync
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
1 parent 3ef8b3c commit 668daf5

4 files changed

Lines changed: 190 additions & 5 deletions

File tree

‎build/build.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,10 @@ func detectSharedMounts(ctx context.Context, reqs map[string][]*reqForNode) (_ m
13501350
m := map[string]map[fsKey]*fsTracker{}
13511351
for _, reqs := range reqs {
13521352
for _, req := range reqs {
1353+
// Shared providers cannot apply each solve's filter hook.
1354+
if req.so.LocalFilterOpt != nil {
1355+
continue
1356+
}
13531357
nodeName := req.Node().Name
13541358
// skip shared-session optimisation: targets may connect to different replicas.
13551359
if req.Node().Driver != nil && req.Node().Driver.RequiresUncachedClient() {

‎build/opt.go‎

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import (
5050
"github.com/moby/buildkit/util/archiveutil"
5151
"github.com/moby/buildkit/util/entitlements"
5252
"github.com/moby/buildkit/util/gitutil"
53+
"github.com/moby/patternmatcher"
54+
"github.com/moby/patternmatcher/ignorefile"
5355
"github.com/opencontainers/go-digest"
5456
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
5557
"github.com/pkg/errors"
@@ -554,7 +556,7 @@ func toSolveOpt(ctx context.Context, np *noderesolver.ResolvedNode, multiDriver
554556
so.FrontendAttrs["label:"+k] = v
555557
}
556558

557-
releaseLoad, err := loadInputs(ctx, nodeDriver, &opt.Inputs, pw, &so)
559+
releaseLoad, err := loadInputs(ctx, nodeDriver, &opt.Inputs, cfg, pw, &so)
558560
if err != nil {
559561
return nil, nil, err
560562
}
@@ -829,7 +831,7 @@ func policyEnvFilename(inp Inputs) string {
829831
return "Dockerfile"
830832
}
831833

832-
func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw progress.Writer, target *client.SolveOpt) (func(), error) {
834+
func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, cfg *confutil.Config, pw progress.Writer, target *client.SolveOpt) (func(), error) {
833835
if inp.ContextPath == "" {
834836
return nil, errors.New("please specify build context (e.g. \".\" for the current directory)")
835837
}
@@ -891,7 +893,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
891893
}
892894
}
893895
case osutil.IsLocalDir(inp.ContextPath):
894-
if err := setLocalMount("context", inp.ContextPath, target); err != nil {
896+
if err := setLocalContextMount("context", inp.ContextPath, cfg, target); err != nil {
895897
return nil, err
896898
}
897899
contextDir = inp.ContextPath
@@ -1071,7 +1073,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro
10711073
if k == "context" || k == "dockerfile" {
10721074
localName = "_" + k // underscore to avoid collisions
10731075
}
1074-
if err := setLocalMount(localName, v.Path, target); err != nil {
1076+
if err := setLocalContextMount(localName, v.Path, cfg, target); err != nil {
10751077
return nil, err
10761078
}
10771079
target.FrontendAttrs["context:"+k] = "local:" + localName
@@ -1163,6 +1165,50 @@ func setLocalMount(name, dir string, so *client.SolveOpt) error {
11631165
return nil
11641166
}
11651167

1168+
func setLocalContextMount(name, dir string, cfg *confutil.Config, so *client.SolveOpt) error {
1169+
if err := setLocalMount(name, dir, so); err != nil {
1170+
return err
1171+
}
1172+
1173+
filename := filepath.Join(cfg.Dir(), ".dockerignore")
1174+
f, err := os.Open(filename)
1175+
if errors.Is(err, os.ErrNotExist) {
1176+
return nil
1177+
}
1178+
if err != nil {
1179+
return errors.Wrapf(err, "failed to open global ignore file %s", filename)
1180+
}
1181+
defer f.Close()
1182+
1183+
patterns, err := ignorefile.ReadAll(f)
1184+
if err != nil {
1185+
return errors.Wrapf(err, "failed to read global ignore file %s", filename)
1186+
}
1187+
if len(patterns) == 0 {
1188+
return nil
1189+
}
1190+
// BuildKit reads the context's ignore file through this mount before
1191+
// applying its repository-specific rules.
1192+
patterns = append(patterns, "!.dockerignore")
1193+
1194+
if _, err := patternmatcher.New(patterns); err != nil {
1195+
return errors.Wrapf(err, "failed to parse global ignore file %s", filename)
1196+
}
1197+
previous := so.LocalFilterOpt
1198+
so.LocalFilterOpt = func(dirName string, opt *fsutil.FilterOpt) error {
1199+
if previous != nil {
1200+
if err := previous(dirName, opt); err != nil {
1201+
return err
1202+
}
1203+
}
1204+
if dirName == name {
1205+
opt.ExcludePatterns = append(slices.Clone(patterns), opt.ExcludePatterns...)
1206+
}
1207+
return nil
1208+
}
1209+
return nil
1210+
}
1211+
11661212
func createTempDockerfile(r io.Reader, multiReader *SyncMultiReader) (string, error) {
11671213
dir, err := os.MkdirTemp("", "dockerfile")
11681214
if err != nil {

‎build/opt_test.go‎

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package build
22

33
import (
44
"context"
5+
"io"
6+
"os"
7+
"path/filepath"
58
"sync"
69
"testing"
710

811
"github.com/docker/buildx/policy"
912
"github.com/docker/buildx/util/buildflags"
13+
"github.com/docker/buildx/util/confutil"
1014
"github.com/docker/buildx/util/ocilayout"
1115
"github.com/docker/buildx/util/progress"
1216
"github.com/moby/buildkit/client"
@@ -19,8 +23,94 @@ import (
1923
"github.com/pkg/errors"
2024
"github.com/stretchr/testify/assert"
2125
"github.com/stretchr/testify/require"
26+
"github.com/tonistiigi/fsutil"
2227
)
2328

29+
func TestGlobalDockerignore(t *testing.T) {
30+
contextDir := t.TempDir()
31+
configDir := t.TempDir()
32+
for name, content := range map[string]string{
33+
"keep.txt": "keep",
34+
"drop.txt": "drop",
35+
".dockerignore": "*.log\n",
36+
"debug.log": "log",
37+
".idea/state": "editor",
38+
".idea/keep": "editor",
39+
} {
40+
path := filepath.Join(contextDir, name)
41+
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
42+
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
43+
}
44+
require.NoError(t, os.WriteFile(filepath.Join(configDir, ".dockerignore"), []byte(".idea/*\n!.idea/keep\n.dockerignore\n*.txt\n"), 0o644))
45+
46+
so := &client.SolveOpt{}
47+
require.NoError(t, setLocalContextMount("context", contextDir, confutil.NewConfig(nil, confutil.WithDir(configDir)), so))
48+
opt := &fsutil.FilterOpt{ExcludePatterns: []string{"!keep.txt"}}
49+
require.NoError(t, so.LocalFilterOpt("context", opt))
50+
filtered, err := fsutil.NewFilterFS(so.LocalMounts["context"], opt)
51+
require.NoError(t, err)
52+
for _, name := range []string{"keep.txt", ".dockerignore", "debug.log", ".idea/keep"} {
53+
f, err := filtered.Open(name)
54+
require.NoError(t, err, name)
55+
_, err = io.ReadAll(f)
56+
require.NoError(t, err)
57+
require.NoError(t, f.Close())
58+
}
59+
_, err = filtered.Open(".idea/state")
60+
require.ErrorIs(t, err, os.ErrNotExist)
61+
_, err = filtered.Open("drop.txt")
62+
require.ErrorIs(t, err, os.ErrNotExist)
63+
64+
// A missing global file leaves local mounts unchanged.
65+
require.NoError(t, setLocalContextMount("other", contextDir, confutil.NewConfig(nil, confutil.WithDir(t.TempDir())), so))
66+
otherOpt := &fsutil.FilterOpt{}
67+
require.NoError(t, so.LocalFilterOpt("other", otherOpt))
68+
require.Empty(t, otherOpt.ExcludePatterns)
69+
f, err := so.LocalMounts["other"].Open(".idea/state")
70+
require.NoError(t, err)
71+
require.NoError(t, f.Close())
72+
}
73+
74+
func TestGlobalDockerignoreInvalidPattern(t *testing.T) {
75+
configDir := t.TempDir()
76+
require.NoError(t, os.WriteFile(filepath.Join(configDir, ".dockerignore"), []byte("[\n"), 0o644))
77+
err := setLocalContextMount("context", t.TempDir(), confutil.NewConfig(nil, confutil.WithDir(configDir)), &client.SolveOpt{})
78+
require.ErrorContains(t, err, "failed to parse global ignore file")
79+
}
80+
81+
func TestLoadInputsGlobalDockerignore(t *testing.T) {
82+
contextDir := t.TempDir()
83+
namedDir := t.TempDir()
84+
configDir := t.TempDir()
85+
for _, dir := range []string{contextDir, namedDir} {
86+
require.NoError(t, os.WriteFile(filepath.Join(dir, "local.txt"), []byte("local"), 0o644))
87+
}
88+
require.NoError(t, os.WriteFile(filepath.Join(contextDir, "Containerfile"), []byte("FROM scratch\n"), 0o644))
89+
require.NoError(t, os.WriteFile(filepath.Join(configDir, ".dockerignore"), []byte("local.txt\n"), 0o644))
90+
91+
so := &client.SolveOpt{FrontendAttrs: map[string]string{}}
92+
inp := &Inputs{ContextPath: contextDir, DockerfilePath: filepath.Join(contextDir, "Containerfile"), NamedContexts: map[string]NamedContext{
93+
"source": {Path: namedDir},
94+
}}
95+
release, err := loadInputs(context.Background(), nil, inp, confutil.NewConfig(nil, confutil.WithDir(configDir)), testProgressWriter{}, so)
96+
require.NoError(t, err)
97+
t.Cleanup(release)
98+
for _, name := range []string{"context", "source"} {
99+
opt := &fsutil.FilterOpt{}
100+
require.NoError(t, so.LocalFilterOpt(name, opt))
101+
filtered, err := fsutil.NewFilterFS(so.LocalMounts[name], opt)
102+
require.NoError(t, err)
103+
_, err = filtered.Open("local.txt")
104+
require.ErrorIs(t, err, os.ErrNotExist)
105+
}
106+
dockerfileOpt := &fsutil.FilterOpt{}
107+
require.NoError(t, so.LocalFilterOpt("dockerfile", dockerfileOpt))
108+
require.Empty(t, dockerfileOpt.ExcludePatterns)
109+
f, err := so.LocalMounts["dockerfile"].Open("local.txt")
110+
require.NoError(t, err)
111+
require.NoError(t, f.Close())
112+
}
113+
24114
func TestCacheOptions_DerivedVars(t *testing.T) {
25115
t.Setenv("ACTIONS_RUNTIME_TOKEN", "sensitive_token")
26116
t.Setenv("ACTIONS_CACHE_URL", "https://cache.github.com")
@@ -284,7 +374,7 @@ func TestLoadInputsOCILayoutNamedContext(t *testing.T) {
284374
},
285375
}
286376

287-
release, err := loadInputs(context.Background(), nil, inp, testProgressWriter{}, target)
377+
release, err := loadInputs(context.Background(), nil, inp, nil, testProgressWriter{}, target)
288378
require.NoError(t, err)
289379
require.NotNil(t, release)
290380
t.Cleanup(release)

‎tests/build.go‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func buildCmd(sb integration.Sandbox, opts ...cmdOpt) (string, error) {
5353

5454
var buildTests = []func(t *testing.T, sb integration.Sandbox){
5555
testBuild,
56+
testBuildGlobalDockerignore,
5657
testBuildAlias,
5758
testBuildStdin,
5859
testBuildRemote,
@@ -103,6 +104,50 @@ func testBuild(t *testing.T, sb integration.Sandbox) {
103104
require.NoError(t, err, string(out))
104105
}
105106

107+
func testBuildGlobalDockerignore(t *testing.T, sb integration.Sandbox) {
108+
contextDir := tmpdir(t,
109+
fstest.CreateFile("Dockerfile", []byte("FROM scratch\nCOPY . /main/\nCOPY --from=extra . /named/\n"), 0o600),
110+
fstest.CreateFile(".dockerignore", []byte("repo-only.bin\n!keep.txt\n"), 0o600),
111+
fstest.CreateFile("keep.txt", []byte("main"), 0o600),
112+
fstest.CreateFile("drop.txt", []byte("global"), 0o600),
113+
fstest.CreateFile("repo-only.bin", []byte("repo"), 0o600),
114+
fstest.CreateDir(".idea", 0o700),
115+
fstest.CreateFile(".idea/state", []byte("editor"), 0o600),
116+
)
117+
namedDir := tmpdir(t,
118+
fstest.CreateFile("keep.bin", []byte("named"), 0o600),
119+
fstest.CreateFile("drop.txt", []byte("global"), 0o600),
120+
fstest.CreateDir(".idea", 0o700),
121+
fstest.CreateFile(".idea/state", []byte("editor"), 0o600),
122+
)
123+
configDir := buildxConfig(sb)
124+
require.NotEmpty(t, configDir)
125+
require.NoError(t, os.MkdirAll(configDir, 0o700))
126+
globalIgnore := filepath.Join(configDir, ".dockerignore")
127+
require.NoError(t, os.WriteFile(globalIgnore, []byte(".idea\n*.txt\n.dockerignore\n"), 0o600))
128+
t.Cleanup(func() { _ = os.Remove(globalIgnore) })
129+
130+
dest := t.TempDir()
131+
out, err := buildCmd(sb, withArgs(
132+
"--build-context", "extra="+namedDir,
133+
"--output=type=local,dest="+dest,
134+
contextDir,
135+
))
136+
require.NoError(t, err, out)
137+
for path, want := range map[string]string{
138+
"main/keep.txt": "main",
139+
"named/keep.bin": "named",
140+
} {
141+
data, err := os.ReadFile(filepath.Join(dest, path))
142+
require.NoError(t, err)
143+
require.Equal(t, want, string(data))
144+
}
145+
for _, path := range []string{"main/.idea/state", "named/.idea/state", "main/repo-only.bin", "main/drop.txt", "named/drop.txt"} {
146+
_, err := os.Stat(filepath.Join(dest, path))
147+
require.ErrorIs(t, err, os.ErrNotExist)
148+
}
149+
}
150+
106151
func testBuildAlias(t *testing.T, sb integration.Sandbox) {
107152
dir := createTestProject(t)
108153
cmd := buildxCmd(sb, withDir(dir), withArgs("b", dir))

0 commit comments

Comments
 (0)