diff --git a/client/llb/fileop.go b/client/llb/fileop.go index 18d1d68e1..731aa6199 100644 --- a/client/llb/fileop.go +++ b/client/llb/fileop.go @@ -4,6 +4,8 @@ import ( _ "crypto/sha256" "os" "path" + "strconv" + "strings" "time" "github.com/moby/buildkit/solver/pb" @@ -29,11 +31,12 @@ import ( // filestate = state.File(c) // filestate.GetOutput(id).Exec() -func NewFileOp(s State, action *FileAction) *FileOp { +func NewFileOp(s State, action *FileAction, c Constraints) *FileOp { action = action.bind(s) f := &FileOp{ - action: action, + action: action, + constraints: c, } f.output = &output{vertex: f, getIndex: func() (pb.OutputIndex, error) { @@ -190,9 +193,40 @@ func (mi *MkdirInfo) SetMkdirOption(mi2 *MkdirInfo) { } func WithUser(name string) ChownOption { - return ChownOpt{ - User: &UserOpt{Name: name}, + opt := ChownOpt{} + + parts := strings.SplitN(name, ":", 2) + for i, v := range parts { + switch i { + case 0: + uid, err := parseUID(v) + if err != nil { + opt.User = &UserOpt{Name: v} + } else { + opt.User = &UserOpt{UID: uid} + } + case 1: + gid, err := parseUID(v) + if err != nil { + opt.Group = &UserOpt{Name: v} + } else { + opt.Group = &UserOpt{UID: gid} + } + } } + + return opt +} + +func parseUID(str string) (int, error) { + if str == "root" { + return 0, nil + } + uid, err := strconv.ParseInt(str, 10, 32) + if err != nil { + return 0, err + } + return int(uid), nil } func WithUIDGID(uid, gid int) ChownOption { @@ -479,7 +513,7 @@ type FileOp struct { action *FileAction output Output - // constraints Constraints + constraints Constraints isValidated bool } @@ -610,7 +644,7 @@ func (f *FileOp) Marshal(c *Constraints) (digest.Digest, []byte, *pb.OpMetadata, pfo := &pb.FileOp{} - pop, md := MarshalConstraints(c, &Constraints{}) + pop, md := MarshalConstraints(c, &f.constraints) pop.Op = &pb.Op_File{ File: pfo, } diff --git a/client/llb/state.go b/client/llb/state.go index 5929e19a5..0336b7fd1 100644 --- a/client/llb/state.go +++ b/client/llb/state.go @@ -229,8 +229,13 @@ func (s State) Run(ro ...RunOption) ExecState { } } -func (s State) File(a *FileAction) State { - return s.WithOutput(NewFileOp(s, a).Output()) +func (s State) File(a *FileAction, opts ...ConstraintsOpt) State { + var c Constraints + for _, o := range opts { + o.SetConstraintsOption(&c) + } + + return s.WithOutput(NewFileOp(s, a, c).Output()) } func (s State) AddEnv(key, value string) State { diff --git a/examples/dockerfile2llb/main.go b/examples/dockerfile2llb/main.go index 9b31ea5d3..41d2d4d2b 100644 --- a/examples/dockerfile2llb/main.go +++ b/examples/dockerfile2llb/main.go @@ -9,6 +9,7 @@ import ( "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb/imagemetaresolver" "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb" + "github.com/moby/buildkit/solver/pb" "github.com/moby/buildkit/util/appcontext" ) @@ -26,9 +27,12 @@ func main() { panic(err) } + caps := pb.Caps.CapSet(pb.Caps.All()) + state, img, err := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{ MetaResolver: imagemetaresolver.Default(), Target: opt.target, + LLBCaps: &caps, }) if err != nil { log.Printf("err: %+v", err) diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 42ea06146..cb9b1e7af 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -151,6 +151,10 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, switch cmd.(type) { case *instructions.AddCommand, *instructions.CopyCommand, *instructions.RunCommand: total++ + case *instructions.WorkdirCommand: + if useFileOp(opt.BuildArgs, opt.LLBCaps) { + total++ + } } } ds.cmdTotal = total @@ -307,7 +311,7 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, d.state = d.state.AddEnv(k, v) } if d.image.Config.WorkingDir != "" { - if err = dispatchWorkdir(d, &instructions.WorkdirCommand{Path: d.image.Config.WorkingDir}, false); err != nil { + if err = dispatchWorkdir(d, &instructions.WorkdirCommand{Path: d.image.Config.WorkingDir}, false, nil); err != nil { return nil, nil, err } } @@ -468,7 +472,7 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error { case *instructions.RunCommand: err = dispatchRun(d, c, opt.proxyEnv, cmd.sources, opt) case *instructions.WorkdirCommand: - err = dispatchWorkdir(d, c, true) + err = dispatchWorkdir(d, c, true, &opt) case *instructions.AddCommand: err = dispatchCopy(d, c.SourcesAndDest, opt.buildContext, true, c, "", opt) if err == nil { @@ -648,7 +652,7 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE return commitToHistory(&d.image, "RUN "+runCommandString(args, d.buildArgs), true, &d.state) } -func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bool) error { +func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bool, opt *dispatchOpt) error { d.state = d.state.Dir(c.Path) wd := c.Path if !path.IsAbs(c.Path) { @@ -656,13 +660,114 @@ func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bo } d.image.Config.WorkingDir = wd if commit { + if opt != nil && useFileOp(opt.buildArgValues, opt.llbCaps) { + mkdirOpt := []llb.MkdirOption{llb.WithParents(true)} + if user := d.image.Config.User; user != "" { + mkdirOpt = append(mkdirOpt, llb.WithUser(user)) + } + platform := opt.targetPlatform + if d.platform != nil { + platform = *d.platform + } + d.state = d.state.File(llb.Mkdir(wd, 0755, mkdirOpt...), llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, c.String(), d.state.Env())), d.prefixPlatform, &platform))) + } + return commitToHistory(&d.image, "WORKDIR "+wd, false, nil) } return nil } +func dispatchCopyFileOp(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, opt dispatchOpt) error { + dest := path.Join("/", pathRelativeToWorkingDir(d.state, c.Dest())) + if c.Dest() == "." || c.Dest() == "" || c.Dest()[len(c.Dest())-1] == filepath.Separator { + dest += string(filepath.Separator) + } + + var copyOpt []llb.CopyOption + + if chown != "" { + copyOpt = append(copyOpt, llb.WithUser(chown)) + } + + commitMessage := bytes.NewBufferString("") + if isAddCommand { + commitMessage.WriteString("ADD") + } else { + commitMessage.WriteString("COPY") + } + + var a *llb.FileAction + + for _, src := range c.Sources() { + commitMessage.WriteString(" " + src) + if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { + if !isAddCommand { + return errors.New("source can't be a URL for COPY") + } + + // Resources from remote URLs are not decompressed. + // https://docs.docker.com/engine/reference/builder/#add + // + // Note: mixing up remote archives and local archives in a single ADD instruction + // would result in undefined behavior: https://github.com/moby/buildkit/pull/387#discussion_r189494717 + u, err := url.Parse(src) + f := "__unnamed__" + if err == nil { + if base := path.Base(u.Path); base != "." && base != "/" { + f = base + } + } + + st := llb.HTTP(src, llb.Filename(f), dfCmd(c)) + + opts := append([]llb.CopyOption{&llb.CopyInfo{ + CreateDestPath: true, + }}, copyOpt...) + + if a == nil { + a = llb.Copy(st, f, dest, opts...) + } else { + a = a.Copy(st, f, dest, opts...) + } + } else { + opts := append([]llb.CopyOption{&llb.CopyInfo{ + FollowSymlinks: true, + CopyDirContentsOnly: true, + AttemptUnpack: isAddCommand, + CreateDestPath: true, + AllowWildcard: true, + AllowEmptyWildcard: true, + }}, copyOpt...) + + if a == nil { + a = llb.Copy(sourceState, src, dest, opts...) + } else { + a = a.Copy(sourceState, src, dest, opts...) + } + } + } + + commitMessage.WriteString(" " + c.Dest()) + + platform := opt.targetPlatform + if d.platform != nil { + platform = *d.platform + } + + fileOpt := []llb.ConstraintsOpt{llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, cmdToPrint.String(), d.state.Env())), d.prefixPlatform, &platform))} + if d.ignoreCache { + fileOpt = append(fileOpt, llb.IgnoreCache) + } + + d.state = d.state.File(a, fileOpt...) + return commitToHistory(&d.image, commitMessage.String(), true, &d.state) +} + func dispatchCopy(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, opt dispatchOpt) error { - // TODO: this should use CopyOp instead. Current implementation is inefficient + if useFileOp(opt.buildArgValues, opt.llbCaps) { + return dispatchCopyFileOp(d, c, sourceState, isAddCommand, cmdToPrint, chown, opt) + } + img := llb.Image(opt.copyImage, llb.MarkImageInternal, llb.Platform(opt.buildPlatforms[0]), WithInternalName("helper image for file operations")) dest := path.Join(".", pathRelativeToWorkingDir(d.state, c.Dest())) @@ -1176,3 +1281,13 @@ func prefixCommand(ds *dispatchState, str string, prefixPlatform bool, platform out += fmt.Sprintf("%d/%d] ", ds.cmdIndex, ds.cmdTotal) return out + str } + +func useFileOp(args map[string]string, caps *apicaps.CapSet) bool { + enabled := fileOpEnabled + if v, ok := args["BUILDKIT_USE_FILEOP"]; ok { + if b, err := strconv.ParseBool(v); err != nil { + enabled = b + } + } + return enabled && caps != nil && caps.Supports(pb.CapFileBase) == nil +} diff --git a/frontend/dockerfile/dockerfile2llb/convert_fileop.go b/frontend/dockerfile/dockerfile2llb/convert_fileop.go new file mode 100644 index 000000000..9c8abc7c3 --- /dev/null +++ b/frontend/dockerfile/dockerfile2llb/convert_fileop.go @@ -0,0 +1,5 @@ +// +build fileop + +package dockerfile2llb + +const fileOpEnabled = true diff --git a/frontend/dockerfile/dockerfile2llb/convert_nofileop.go b/frontend/dockerfile/dockerfile2llb/convert_nofileop.go new file mode 100644 index 000000000..f8f7582b0 --- /dev/null +++ b/frontend/dockerfile/dockerfile2llb/convert_nofileop.go @@ -0,0 +1,5 @@ +// +build !fileop + +package dockerfile2llb + +const fileOpEnabled = false diff --git a/solver/llbsolver/ops/file.go b/solver/llbsolver/ops/file.go index 013778fe0..379d1df9a 100644 --- a/solver/llbsolver/ops/file.go +++ b/solver/llbsolver/ops/file.go @@ -46,15 +46,23 @@ func NewFileOp(v solver.Vertex, op *pb.Op_File, cm cache.Manager, md *metadata.S func (f *fileOp) CacheMap(ctx context.Context, index int) (*solver.CacheMap, bool, error) { selectors := map[int]map[llbsolver.Selector]struct{}{} + invalidSelectors := map[int]struct{}{} actions := make([][]byte, 0, len(f.op.Actions)) + markInvalid := func(idx pb.InputIndex) { + if idx != -1 { + invalidSelectors[int(idx)] = struct{}{} + } + } + for _, action := range f.op.Actions { var dt []byte var err error switch a := action.Action.(type) { case *pb.FileAction_Mkdir: p := *a.Mkdir + markInvalid(action.Input) processOwner(p.Owner, selectors) dt, err = json.Marshal(p) if err != nil { @@ -62,7 +70,7 @@ func (f *fileOp) CacheMap(ctx context.Context, index int) (*solver.CacheMap, boo } case *pb.FileAction_Mkfile: p := *a.Mkfile - p.Owner = nil + markInvalid(action.Input) processOwner(p.Owner, selectors) dt, err = json.Marshal(p) if err != nil { @@ -70,14 +78,15 @@ func (f *fileOp) CacheMap(ctx context.Context, index int) (*solver.CacheMap, boo } case *pb.FileAction_Rm: p := *a.Rm + markInvalid(action.Input) dt, err = json.Marshal(p) if err != nil { return nil, false, err } case *pb.FileAction_Copy: p := *a.Copy + markInvalid(action.Input) processOwner(p.Owner, selectors) - p.Owner = nil if action.SecondaryInput != -1 && int(action.SecondaryInput) < f.numInputs { p.Src = path.Base(p.Src) addSelector(selectors, int(action.SecondaryInput), p.Src, p.AllowWildcard, p.FollowSymlink) @@ -111,6 +120,9 @@ func (f *fileOp) CacheMap(ctx context.Context, index int) (*solver.CacheMap, boo } for idx, m := range selectors { + if _, ok := invalidSelectors[idx]; ok { + continue + } dgsts := make([][]byte, 0, len(m)) for k := range m { dgsts = append(dgsts, []byte(k.Path)) diff --git a/solver/pb/caps.go b/solver/pb/caps.go index b81ec62c4..add5c17ed 100644 --- a/solver/pb/caps.go +++ b/solver/pb/caps.go @@ -43,6 +43,8 @@ const ( CapExecMountSSH apicaps.CapID = "exec.mount.ssh" CapExecCgroupsMounted apicaps.CapID = "exec.cgroup" + CapFileBase apicaps.CapID = "file.base" + CapConstraints apicaps.CapID = "constraints" CapPlatform apicaps.CapID = "platform" @@ -226,6 +228,12 @@ func init() { Status: apicaps.CapStatusExperimental, }) + Caps.Init(apicaps.Cap{ + ID: CapFileBase, + Enabled: true, + Status: apicaps.CapStatusPrerelease, + }) + Caps.Init(apicaps.Cap{ ID: CapConstraints, Enabled: true,