diff --git a/cache/contenthash/checksum.go b/cache/contenthash/checksum.go index 686555351..f9641a5a0 100644 --- a/cache/contenthash/checksum.go +++ b/cache/contenthash/checksum.go @@ -12,6 +12,7 @@ import ( "sync" "sync/atomic" + cerrdefs "github.com/containerd/errdefs" iradix "github.com/hashicorp/go-immutable-radix/v2" simplelru "github.com/hashicorp/golang-lru/v2/simplelru" "github.com/moby/buildkit/cache" @@ -59,6 +60,7 @@ type ChecksumOpts struct { Wildcard bool IncludePatterns []string ExcludePatterns []string + RequiredPaths []string } func Checksum(ctx context.Context, ref cache.ImmutableRef, path string, opts ChecksumOpts, s session.Group) (digest.Digest, error) { @@ -690,6 +692,17 @@ func (cc *cacheContext) includedPaths(ctx context.Context, m *mount, p string, o cc.tree = txn.Commit() cc.dirty = updated + // Validate that all required paths exist. + for _, requiredPath := range opts.RequiredPaths { + found := slices.ContainsFunc(includedPaths, func(includedPath *includedPath) bool { + return strings.HasPrefix(includedPath.path, requiredPath) + }) + + if !found { + return "", nil, errors.Wrapf(cerrdefs.ErrNotFound, "%q", requiredPath) + } + } + return origPrefix, includedPaths, nil } diff --git a/client/llb/fileop.go b/client/llb/fileop.go index 689eda0b0..2f16432b4 100644 --- a/client/llb/fileop.go +++ b/client/llb/fileop.go @@ -569,6 +569,7 @@ type CopyInfo struct { CopyDirContentsOnly bool IncludePatterns []string ExcludePatterns []string + RequiredPaths []string AttemptUnpack bool CreateDestPath bool AllowWildcard bool @@ -603,6 +604,7 @@ func (a *fileActionCopy) toProtoAction(ctx context.Context, parent string, base Owner: a.info.ChownOpt.marshal(base), IncludePatterns: a.info.IncludePatterns, ExcludePatterns: a.info.ExcludePatterns, + RequiredPaths: a.info.RequiredPaths, AllowWildcard: a.info.AllowWildcard, AllowEmptyWildcard: a.info.AllowEmptyWildcard, FollowSymlink: a.info.FollowSymlinks, @@ -647,6 +649,9 @@ func (a *fileActionCopy) addCaps(f *FileOp) { if len(a.info.IncludePatterns) != 0 || len(a.info.ExcludePatterns) != 0 { addCap(&f.constraints, pb.CapFileCopyIncludeExcludePatterns) } + if len(a.info.RequiredPaths) != 0 { + addCap(&f.constraints, pb.CapFileCopyRequiredPaths) + } if a.info.AlwaysReplaceExistingDestPaths { addCap(&f.constraints, pb.CapFileCopyAlwaysReplaceExistingDestPaths) } diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 665bdb399..001234f2a 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -1223,7 +1223,7 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE // Run command can potentially access any file. Mark the full filesystem as used. d.paths["/"] = struct{}{} - var args = c.CmdLine + args := c.CmdLine if len(c.Files) > 0 { if len(args) != 1 || !c.PrependShell { return errors.Errorf("parsing produced an invalid run command: %v", args) @@ -1606,6 +1606,7 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error { } else { validateCopySourcePath(src, &cfg) var patterns []string + var requiredPaths []string if cfg.parents { // detect optional pivot point parent, pattern, ok := strings.Cut(src, "/./") @@ -1622,6 +1623,12 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error { } patterns = []string{strings.TrimPrefix(pattern, "/")} + + // determine if we want to require any paths to exist. + // we only require a path to exist if wildcards aren't present. + if !containsWildcards(src) && !containsWildcards(pattern) { + requiredPaths = []string{filepath.Join(src, pattern)} + } } src, err = system.NormalizePath("/", src, d.platform.OS, false) @@ -1639,6 +1646,7 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error { FollowSymlinks: true, CopyDirContentsOnly: true, IncludePatterns: patterns, + RequiredPaths: requiredPaths, AttemptUnpack: unpack, CreateDestPath: true, AllowWildcard: true, @@ -1760,7 +1768,7 @@ func dispatchOnbuild(d *dispatchState, c *instructions.OnbuildCommand) error { func dispatchCmd(d *dispatchState, c *instructions.CmdCommand, lint *linter.Linter) error { validateUsedOnce(c, &d.cmd, lint) - var args = c.CmdLine + args := c.CmdLine if c.PrependShell { if len(d.image.Config.Shell) == 0 { msg := linter.RuleJSONArgsRecommended.Format(c.Name()) @@ -1776,7 +1784,7 @@ func dispatchCmd(d *dispatchState, c *instructions.CmdCommand, lint *linter.Lint func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand, lint *linter.Linter) error { validateUsedOnce(c, &d.entrypoint, lint) - var args = c.CmdLine + args := c.CmdLine if c.PrependShell { if len(d.image.Config.Shell) == 0 { msg := linter.RuleJSONArgsRecommended.Format(c.Name()) @@ -2692,3 +2700,15 @@ func (emptyEnvs) Get(string) (string, bool) { func (emptyEnvs) Keys() []string { return nil } + +func containsWildcards(name string) bool { + for i := 0; i < len(name); i++ { + switch name[i] { + case '*', '?', '[': + return true + case '\\': + i++ + } + } + return false +} diff --git a/frontend/dockerfile/dockerfile_parents_test.go b/frontend/dockerfile/dockerfile_parents_test.go index 7a6ac2612..f52a7142d 100644 --- a/frontend/dockerfile/dockerfile_parents_test.go +++ b/frontend/dockerfile/dockerfile_parents_test.go @@ -18,6 +18,7 @@ import ( var parentsTests = integration.TestFuncs( testCopyParents, testCopyRelativeParents, + testCopyParentsMissingDirectory, ) func init() { @@ -180,3 +181,100 @@ eot require.NoError(t, err) } } + +func testCopyParentsMissingDirectory(t *testing.T, sb integration.Sandbox) { + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM alpine AS base +WORKDIR /test +RUN < 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -2578,6 +2583,15 @@ func (this *FileActionCopy) EqualVT(that *FileActionCopy) bool { if this.ModeStr != that.ModeStr { return false } + if len(this.RequiredPaths) != len(that.RequiredPaths) { + return false + } + for i, vx := range this.RequiredPaths { + vy := that.RequiredPaths[i] + if vx != vy { + return false + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -5183,6 +5197,17 @@ func (m *FileActionCopy) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.RequiredPaths) > 0 { + for iNdEx := len(m.RequiredPaths) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.RequiredPaths[iNdEx]) + copy(dAtA[i:], m.RequiredPaths[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.RequiredPaths[iNdEx]))) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x82 + } + } if len(m.ModeStr) > 0 { i -= len(m.ModeStr) copy(dAtA[i:], m.ModeStr) @@ -6950,6 +6975,12 @@ func (m *FileActionCopy) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.RequiredPaths) > 0 { + for _, s := range m.RequiredPaths { + l = len(s) + n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } n += len(m.unknownFields) return n } @@ -13313,6 +13344,38 @@ func (m *FileActionCopy) UnmarshalVT(dAtA []byte) error { } m.ModeStr = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 16: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RequiredPaths", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RequiredPaths = append(m.RequiredPaths, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:])