Skip to content

Commit

Permalink
dockerfile2llb: filter unused paths for named contexts
Browse files Browse the repository at this point in the history
This changes the llb generation in `dockerfile2llb` to add the
`llb.FollowPaths` option when named contexts are used in the same way
that happens with the default context.

This means that dockerfiles that look like this:

```
FROM scratch
COPY --from=mynamedctx ./a.txt /a.txt
```

Will only load the file `a.txt`. This behavior is consistent with the
default context.
  • Loading branch information
jsternberg committed Aug 17, 2023
1 parent 7f02969 commit 6636a86
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 23 deletions.
128 changes: 105 additions & 23 deletions frontend/dockerfile/dockerfile2llb/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,26 +135,86 @@ func ListTargets(ctx context.Context, dt []byte) (*targets.List, error) {
return l, nil
}

type namedContextState struct {
State llb.State
Image *image.Image

// State used for initializing/re-initializing the state.
name string
out *mutableOutput
copt dockerui.ContextOpt
includePaths map[string]struct{}
}

// newNamedContextState returns an uninitialized namedContextState.
func newNamedContextState(ctx context.Context, client *dockerui.Client, name string, copt dockerui.ContextOpt) (*namedContextState, error) {
out := &mutableOutput{}
nc := &namedContextState{
State: llb.NewState(out),
name: name,
out: out,
copt: copt,
}
return nc.reinit(ctx, client)
}

// reinit will reinitialize this namedContextState. If it is successful, it will return itself.
// If the new state would be invalid, it does not modify the current context and it returns nil along with
// an error if one was present.
func (nc *namedContextState) reinit(ctx context.Context, client *dockerui.Client) (*namedContextState, error) {
// Force a copy of the context options.
copt := nc.copt
if len(nc.includePaths) > 0 {
if includePatterns := normalizeContextPaths(nc.includePaths); includePatterns != nil {
copt.LocalOpts = append(copt.LocalOpts, llb.FollowPaths(includePatterns))
}
}
st, img, err := client.NamedContext(ctx, nc.name, copt)
if err != nil || st == nil {
return nil, err
}

// Update the output with the new state output.
// This only updates the mutableOutput and does not modify the llb.State as that
// state may be used as an input in multiple locations.
nc.out.Output = st.Output()
nc.Image = img
return nc, nil
}

// includePath marks a path to be included in the named context.
//
// Must call reinit to reinitialize this context with the appropriate local path options.
func (nc *namedContextState) includePath(src string) {
if nc.includePaths == nil {
nc.includePaths = make(map[string]struct{})
}
nc.includePaths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
}

func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchState, error) {
if len(dt) == 0 {
return nil, errors.Errorf("the Dockerfile cannot be empty")
}

namedContext := func(ctx context.Context, name string, copt dockerui.ContextOpt) (*llb.State, *image.Image, error) {
namedContext := func(ctx context.Context, name string, copt dockerui.ContextOpt) (*namedContextState, error) {
if opt.Client == nil {
return nil, nil, nil
return nil, nil
}
if !strings.EqualFold(name, "scratch") && !strings.EqualFold(name, "context") {
if copt.Platform == nil {
copt.Platform = opt.TargetPlatform
}
st, img, err := opt.Client.NamedContext(ctx, name, copt)

// Attempt to initialized a named context state with the given options.
// If we succeed, use it.
nc, err := newNamedContextState(ctx, opt.Client, name, copt)
if err != nil {
return nil, nil, err
return nil, err
}
return st, img, nil
return nc, nil
}
return nil, nil, nil
return nil, nil
}

if opt.Warn == nil {
Expand Down Expand Up @@ -255,20 +315,21 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
}

if st.Name != "" {
s, img, err := namedContext(ctx, st.Name, dockerui.ContextOpt{Platform: ds.platform, ResolveMode: opt.ImageResolveMode.String()})
nc, err := namedContext(ctx, st.Name, dockerui.ContextOpt{Platform: ds.platform, ResolveMode: opt.ImageResolveMode.String()})
if err != nil {
return nil, err
}
if s != nil {
if nc != nil {
ds.noinit = true
ds.state = *s
if img != nil {
ds.image = clampTimes(*img, opt.Epoch)
if img.Architecture != "" && img.OS != "" {
ds.state = nc.State
ds.namedCtxState = nc
if nc.Image != nil {
ds.image = clampTimes(*nc.Image, opt.Epoch)
if nc.Image.Architecture != "" && nc.Image.OS != "" {
ds.platform = &ocispecs.Platform{
OS: img.OS,
Architecture: img.Architecture,
Variant: img.Variant,
OS: nc.Image.OS,
Architecture: nc.Image.Architecture,
Variant: nc.Image.Variant,
}
}
}
Expand Down Expand Up @@ -382,17 +443,17 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
d.stage.BaseName = reference.TagNameOnly(ref).String()

var isScratch bool
st, img, err := namedContext(ctx, d.stage.BaseName, dockerui.ContextOpt{ResolveMode: opt.ImageResolveMode.String(), Platform: platform})
nc, err := namedContext(ctx, d.stage.BaseName, dockerui.ContextOpt{ResolveMode: opt.ImageResolveMode.String(), Platform: platform})
if err != nil {
return err
}
if st != nil {
if img != nil {
d.image = *img
if nc != nil {
if nc.Image != nil {
d.image = *nc.Image
} else {
d.image = emptyImage(platformOpt.targetPlatform)
}
d.state = st.Platform(*platform)
d.state = nc.State.Platform(*platform)
d.platform = platform
return nil
}
Expand Down Expand Up @@ -575,6 +636,20 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
buildContext.Output = bctx.Output()
}

// Update the named context definitions if the set of usedPaths was updated.
for _, d := range allDispatchStates.states {
if d.namedCtxState == nil {
continue
}
// Ignore errors here. At this point, we've already got a working
// named context and further work is done to improve performance.
// This should never fail, but there's no point in raising an error
// that is unactionable when we can just continue as normal.
// NOTE: maybe I can figure out how to use warn to raise warnings if this fails?
// Not even sure that's worth it.
d.namedCtxState.reinit(ctx, opt.Client)
}

defaults := []llb.ConstraintsOpt{
llb.Platform(platformOpt.targetPlatform),
}
Expand Down Expand Up @@ -756,9 +831,15 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
location: c.Location(),
opt: opt,
})
if err == nil && len(cmd.sources) == 0 {
for _, src := range c.SourcePaths {
d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
if err == nil {
if len(cmd.sources) == 0 {
for _, src := range c.SourcePaths {
d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{}
}
} else if source := cmd.sources[0]; source.namedCtxState != nil {
for _, src := range c.SourcePaths {
source.namedCtxState.includePath(src)
}
}
}
default:
Expand All @@ -778,6 +859,7 @@ type dispatchState struct {
buildArgs []instructions.KeyValuePairOptional
commands []command
ctxPaths map[string]struct{}
namedCtxState *namedContextState
ignoreCache bool
cmdSet bool
unregistered bool
Expand Down
2 changes: 2 additions & 0 deletions frontend/dockerfile/dockerfile2llb/convert_runmount.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []*

if mount.From == "" {
d.ctxPaths[path.Join("/", filepath.ToSlash(mount.Source))] = struct{}{}
} else if source := sources[i]; source.namedCtxState != nil {
source.namedCtxState.includePath(mount.Source)
}
}
return out, nil
Expand Down

0 comments on commit 6636a86

Please sign in to comment.