Skip to content

Commit 4ab32be

Browse files
authored
Merge pull request #4161 from jsternberg/filtered-named-context
dockerfile2llb: filter unused paths for named contexts
2 parents c0732cc + bee54f0 commit 4ab32be

File tree

5 files changed

+248
-38
lines changed

5 files changed

+248
-38
lines changed

frontend/dockerfile/dockerfile2llb/convert.go

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
153153
if copt.Platform == nil {
154154
copt.Platform = opt.TargetPlatform
155155
}
156-
st, img, err := opt.Client.NamedContext(ctx, name, copt)
157-
if err != nil {
158-
return nil, nil, err
159-
}
160-
return st, img, nil
156+
return opt.Client.NamedContext(ctx, name, copt)
161157
}
162158
return nil, nil, nil
163159
}
@@ -237,6 +233,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
237233
stage: st,
238234
deps: make(map[*dispatchState]struct{}),
239235
ctxPaths: make(map[string]struct{}),
236+
paths: make(map[string]struct{}),
240237
stageName: st.Name,
241238
prefixPlatform: opt.MultiPlatformRequested,
242239
outline: outline.clone(),
@@ -260,7 +257,11 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
260257
}
261258

262259
if st.Name != "" {
263-
s, img, err := namedContext(ctx, st.Name, dockerui.ContextOpt{Platform: ds.platform, ResolveMode: opt.ImageResolveMode.String()})
260+
s, img, err := namedContext(ctx, st.Name, dockerui.ContextOpt{
261+
Platform: ds.platform,
262+
ResolveMode: opt.ImageResolveMode.String(),
263+
AsyncLocalOpts: ds.asyncLocalOpts,
264+
})
264265
if err != nil {
265266
return nil, err
266267
}
@@ -391,7 +392,11 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
391392
d.stage.BaseName = reference.TagNameOnly(ref).String()
392393

393394
var isScratch bool
394-
st, img, err := namedContext(ctx, d.stage.BaseName, dockerui.ContextOpt{ResolveMode: opt.ImageResolveMode.String(), Platform: platform})
395+
st, img, err := namedContext(ctx, d.stage.BaseName, dockerui.ContextOpt{
396+
ResolveMode: opt.ImageResolveMode.String(),
397+
Platform: platform,
398+
AsyncLocalOpts: d.asyncLocalOpts,
399+
})
395400
if err != nil {
396401
return err
397402
}
@@ -492,11 +497,23 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
492497
d.state = d.base.state
493498
d.platform = d.base.platform
494499
d.image = clone(d.base.image)
500+
// Utilize the same path index as our base image so we propagate
501+
// the paths we use back to the base image.
502+
d.paths = d.base.paths
503+
}
504+
505+
// Ensure platform is set.
506+
if d.platform == nil {
507+
d.platform = &d.opt.targetPlatform
495508
}
496509

497510
// make sure that PATH is always set
498511
if _, ok := shell.BuildEnvs(d.image.Config.Env)["PATH"]; !ok {
499-
d.image.Config.Env = append(d.image.Config.Env, "PATH="+system.DefaultPathEnv(d.platform.OS))
512+
var osName string
513+
if d.platform != nil {
514+
osName = d.platform.OS
515+
}
516+
d.image.Config.Env = append(d.image.Config.Env, "PATH="+system.DefaultPathEnv(osName))
500517
}
501518

502519
// initialize base metadata from image conf
@@ -565,17 +582,19 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
565582
}
566583
}
567584

585+
// Ensure the entirety of the target state is marked as used.
586+
// This is done after we've already evaluated every stage to ensure
587+
// the paths attribute is set correctly.
588+
target.paths["/"] = struct{}{}
589+
568590
if len(opt.Labels) != 0 && target.image.Config.Labels == nil {
569591
target.image.Config.Labels = make(map[string]string, len(opt.Labels))
570592
}
571593
for k, v := range opt.Labels {
572594
target.image.Config.Labels[k] = v
573595
}
574596

575-
opts := []llb.LocalOption{}
576-
if includePatterns := normalizeContextPaths(ctxPaths); includePatterns != nil {
577-
opts = append(opts, llb.FollowPaths(includePatterns))
578-
}
597+
opts := filterPaths(ctxPaths)
579598
bctx := opt.MainContext
580599
if opt.Client != nil {
581600
bctx, err = opt.Client.MainContext(ctx, opts...)
@@ -630,6 +649,7 @@ func toCommand(ic instructions.Command, allDispatchStates *dispatchStates) (comm
630649
stn = &dispatchState{
631650
stage: instructions.Stage{BaseName: c.From, Location: ic.Location()},
632651
deps: make(map[*dispatchState]struct{}),
652+
paths: make(map[string]struct{}),
633653
unregistered: true,
634654
}
635655
}
@@ -773,9 +793,19 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
773793
location: c.Location(),
774794
opt: opt,
775795
})
776-
if err == nil && len(cmd.sources) == 0 {
777-
for _, src := range c.SourcePaths {
778-
d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
796+
if err == nil {
797+
if len(cmd.sources) == 0 {
798+
for _, src := range c.SourcePaths {
799+
d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
800+
}
801+
} else {
802+
source := cmd.sources[0]
803+
if source.paths == nil {
804+
source.paths = make(map[string]struct{})
805+
}
806+
for _, src := range c.SourcePaths {
807+
source.paths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
808+
}
779809
}
780810
}
781811
default:
@@ -784,17 +814,20 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
784814
}
785815

786816
type dispatchState struct {
787-
opt dispatchOpt
788-
state llb.State
789-
image image.Image
790-
platform *ocispecs.Platform
791-
stage instructions.Stage
792-
base *dispatchState
793-
noinit bool
794-
deps map[*dispatchState]struct{}
795-
buildArgs []instructions.KeyValuePairOptional
796-
commands []command
797-
ctxPaths map[string]struct{}
817+
opt dispatchOpt
818+
state llb.State
819+
image image.Image
820+
platform *ocispecs.Platform
821+
stage instructions.Stage
822+
base *dispatchState
823+
noinit bool
824+
deps map[*dispatchState]struct{}
825+
buildArgs []instructions.KeyValuePairOptional
826+
commands []command
827+
// ctxPaths marks the paths this dispatchState uses from the build context.
828+
ctxPaths map[string]struct{}
829+
// paths marks the paths that are used by this dispatchState.
830+
paths map[string]struct{}
798831
ignoreCache bool
799832
cmdSet bool
800833
unregistered bool
@@ -808,6 +841,10 @@ type dispatchState struct {
808841
scanContext bool
809842
}
810843

844+
func (ds *dispatchState) asyncLocalOpts() []llb.LocalOption {
845+
return filterPaths(ds.paths)
846+
}
847+
811848
type dispatchStates struct {
812849
states []*dispatchState
813850
statesByName map[string]*dispatchState
@@ -890,6 +927,9 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
890927

891928
customname := c.String()
892929

930+
// Run command can potentially access any file. Mark the full filesystem as used.
931+
d.paths["/"] = struct{}{}
932+
893933
var args []string = c.CmdLine
894934
if len(c.Files) > 0 {
895935
if len(args) != 1 || !c.PrependShell {
@@ -1603,6 +1643,11 @@ func hasCircularDependency(states []*dispatchState) (bool, *dispatchState) {
16031643
}
16041644

16051645
func normalizeContextPaths(paths map[string]struct{}) []string {
1646+
// Avoid a useless allocation if the set of paths is empty.
1647+
if len(paths) == 0 {
1648+
return nil
1649+
}
1650+
16061651
pathSlice := make([]string, 0, len(paths))
16071652
for p := range paths {
16081653
if p == "/" {
@@ -1617,6 +1662,15 @@ func normalizeContextPaths(paths map[string]struct{}) []string {
16171662
return pathSlice
16181663
}
16191664

1665+
// filterPaths returns the local options required to filter an llb.Local
1666+
// to only the required paths.
1667+
func filterPaths(paths map[string]struct{}) []llb.LocalOption {
1668+
if includePaths := normalizeContextPaths(paths); len(includePaths) > 0 {
1669+
return []llb.LocalOption{llb.FollowPaths(includePaths)}
1670+
}
1671+
return nil
1672+
}
1673+
16201674
func proxyEnvFromBuildArgs(args map[string]string) *llb.ProxyEnv {
16211675
pe := &llb.ProxyEnv{}
16221676
isNil := true

frontend/dockerfile/dockerfile2llb/convert_runmount.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func detectRunMount(cmd *command, allDispatchStates *dispatchStates) bool {
3131
stn = &dispatchState{
3232
stage: instructions.Stage{BaseName: from},
3333
deps: make(map[*dispatchState]struct{}),
34+
paths: make(map[string]struct{}),
3435
unregistered: true,
3536
}
3637
}
@@ -136,6 +137,9 @@ func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []*
136137

137138
if mount.From == "" {
138139
d.ctxPaths[path.Join("/", filepath.ToSlash(mount.Source))] = struct{}{}
140+
} else {
141+
source := sources[i]
142+
source.paths[path.Join("/", filepath.ToSlash(mount.Source))] = struct{}{}
139143
}
140144
}
141145
return out, nil

frontend/dockerfile/dockerfile_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@ import (
88
"encoding/json"
99
"fmt"
1010
"io"
11+
"math"
1112
"net/http"
1213
"net/http/httptest"
1314
"os"
1415
"os/exec"
1516
"path"
1617
"path/filepath"
18+
"regexp"
1719
"runtime"
1820
"sort"
1921
"strings"
2022
"testing"
2123
"time"
2224

2325
v1 "github.com/moby/buildkit/cache/remotecache/v1"
26+
"golang.org/x/sync/errgroup"
2427

2528
"github.com/containerd/containerd"
2629
"github.com/containerd/containerd/content"
@@ -130,6 +133,7 @@ var allTests = integration.TestFuncs(
130133
testNamedOCILayoutContextExport,
131134
testNamedInputContext,
132135
testNamedMultiplatformInputContext,
136+
testNamedFilteredContext,
133137
testEmptyDestDir,
134138
testCopyChownCreateDest,
135139
testCopyThroughSymlinkContext,
@@ -6066,6 +6070,114 @@ COPY --from=build /foo /out /
60666070
require.Equal(t, "foo is bar-arm64\n", string(dt))
60676071
}
60686072

6073+
func testNamedFilteredContext(t *testing.T, sb integration.Sandbox) {
6074+
ctx := sb.Context()
6075+
6076+
c, err := client.New(ctx, sb.Address())
6077+
require.NoError(t, err)
6078+
defer c.Close()
6079+
6080+
fooDir := integration.Tmpdir(t,
6081+
// small file
6082+
fstest.CreateFile("foo", []byte(`foo`), 0600),
6083+
// blank file that's just large
6084+
fstest.CreateFile("bar", make([]byte, 4096*1000), 0600),
6085+
)
6086+
6087+
f := getFrontend(t, sb)
6088+
6089+
runTest := func(t *testing.T, dockerfile []byte, target string, min, max int64) {
6090+
t.Run(target, func(t *testing.T) {
6091+
dir := integration.Tmpdir(
6092+
t,
6093+
fstest.CreateFile(dockerui.DefaultDockerfileName, dockerfile, 0600),
6094+
)
6095+
6096+
ch := make(chan *client.SolveStatus)
6097+
6098+
eg, ctx := errgroup.WithContext(sb.Context())
6099+
eg.Go(func() error {
6100+
_, err := f.Solve(ctx, c, client.SolveOpt{
6101+
FrontendAttrs: map[string]string{
6102+
"context:foo": "local:foo",
6103+
"target": target,
6104+
},
6105+
LocalDirs: map[string]string{
6106+
dockerui.DefaultLocalNameDockerfile: dir,
6107+
dockerui.DefaultLocalNameContext: dir,
6108+
"foo": fooDir,
6109+
},
6110+
}, ch)
6111+
return err
6112+
})
6113+
6114+
eg.Go(func() error {
6115+
transferred := make(map[string]int64)
6116+
re := regexp.MustCompile(`transferring (.+):`)
6117+
for ss := range ch {
6118+
for _, status := range ss.Statuses {
6119+
m := re.FindStringSubmatch(status.ID)
6120+
if m == nil {
6121+
continue
6122+
}
6123+
6124+
ctxName := m[1]
6125+
transferred[ctxName] = status.Current
6126+
}
6127+
}
6128+
6129+
if foo := transferred["foo"]; foo < min {
6130+
return errors.Errorf("not enough data was transferred, %d < %d", foo, min)
6131+
} else if foo > max {
6132+
return errors.Errorf("too much data was transferred, %d > %d", foo, max)
6133+
}
6134+
return nil
6135+
})
6136+
6137+
err := eg.Wait()
6138+
require.NoError(t, err)
6139+
})
6140+
}
6141+
6142+
dockerfileBase := []byte(`
6143+
FROM scratch AS copy_from
6144+
COPY --from=foo /foo /
6145+
6146+
FROM alpine AS run_mount
6147+
RUN --mount=from=foo,src=/foo,target=/in/foo cp /in/foo /foo
6148+
6149+
FROM foo AS image_source
6150+
COPY --from=alpine / /
6151+
RUN cat /foo > /bar
6152+
6153+
FROM scratch AS all
6154+
COPY --link --from=copy_from /foo /foo.b
6155+
COPY --link --from=run_mount /foo /foo.c
6156+
COPY --link --from=image_source /bar /foo.d
6157+
`)
6158+
6159+
t.Run("new", func(t *testing.T) {
6160+
runTest(t, dockerfileBase, "run_mount", 1, 1024)
6161+
runTest(t, dockerfileBase, "copy_from", 1, 1024)
6162+
runTest(t, dockerfileBase, "image_source", 4096*1000, math.MaxInt64)
6163+
runTest(t, dockerfileBase, "all", 4096*1000, math.MaxInt64)
6164+
})
6165+
6166+
dockerfileFull := append([]byte(`
6167+
FROM scratch AS foo
6168+
COPY <<EOF /foo
6169+
test
6170+
EOF
6171+
`), dockerfileBase...)
6172+
6173+
t.Run("replace", func(t *testing.T) {
6174+
runTest(t, dockerfileFull, "run_mount", 1, 1024)
6175+
runTest(t, dockerfileFull, "copy_from", 1, 1024)
6176+
runTest(t, dockerfileFull, "image_source", 4096*1000, math.MaxInt64)
6177+
runTest(t, dockerfileFull, "all", 4096*1000, math.MaxInt64)
6178+
})
6179+
}
6180+
60696181
func testSourceDateEpochWithoutExporter(t *testing.T, sb integration.Sandbox) {
60706182
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureSourceDateEpoch)
60716183
f := getFrontend(t, sb)

frontend/dockerui/config.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ type Source struct {
9595

9696
type ContextOpt struct {
9797
NoDockerignore bool
98-
LocalOpts []llb.LocalOption
98+
AsyncLocalOpts func() []llb.LocalOption
9999
Platform *ocispecs.Platform
100100
ResolveMode string
101101
CaptureDigest *digest.Digest
@@ -473,11 +473,8 @@ func (bc *Client) NamedContext(ctx context.Context, name string, opt ContextOpt)
473473
}
474474
pname := name + "::" + platforms.Format(platforms.Normalize(pp))
475475
st, img, err := bc.namedContext(ctx, name, pname, opt)
476-
if err != nil {
477-
return nil, nil, err
478-
}
479-
if st != nil {
480-
return st, img, nil
476+
if err != nil || st != nil {
477+
return st, img, err
481478
}
482479
return bc.namedContext(ctx, name, name, opt)
483480
}

0 commit comments

Comments
 (0)