123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657 |
- package ops
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "path"
- "runtime"
- "sort"
- "sync"
- "github.com/moby/buildkit/cache"
- "github.com/moby/buildkit/cache/metadata"
- "github.com/moby/buildkit/session"
- "github.com/moby/buildkit/solver"
- "github.com/moby/buildkit/solver/llbsolver"
- "github.com/moby/buildkit/solver/llbsolver/errdefs"
- "github.com/moby/buildkit/solver/llbsolver/file"
- "github.com/moby/buildkit/solver/llbsolver/ops/fileoptypes"
- "github.com/moby/buildkit/solver/pb"
- "github.com/moby/buildkit/util/flightcontrol"
- "github.com/moby/buildkit/worker"
- digest "github.com/opencontainers/go-digest"
- "github.com/pkg/errors"
- "golang.org/x/sync/errgroup"
- )
- const fileCacheType = "buildkit.file.v0"
- type fileOp struct {
- op *pb.FileOp
- md *metadata.Store
- w worker.Worker
- solver *FileOpSolver
- numInputs int
- }
- func NewFileOp(v solver.Vertex, op *pb.Op_File, cm cache.Manager, md *metadata.Store, w worker.Worker) (solver.Op, error) {
- if err := llbsolver.ValidateOp(&pb.Op{Op: op}); err != nil {
- return nil, err
- }
- return &fileOp{
- op: op.File,
- md: md,
- numInputs: len(v.Inputs()),
- w: w,
- solver: NewFileOpSolver(w, &file.Backend{}, file.NewRefManager(cm)),
- }, nil
- }
- func (f *fileOp) CacheMap(ctx context.Context, g session.Group, 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{}{}
- }
- }
- indexes := make([][]int, 0, len(f.op.Actions))
- 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 {
- return nil, false, err
- }
- case *pb.FileAction_Mkfile:
- p := *a.Mkfile
- markInvalid(action.Input)
- processOwner(p.Owner, selectors)
- dt, err = json.Marshal(p)
- if err != nil {
- return nil, false, err
- }
- 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)
- if action.SecondaryInput != -1 && int(action.SecondaryInput) < f.numInputs {
- addSelector(selectors, int(action.SecondaryInput), p.Src, p.AllowWildcard, p.FollowSymlink)
- p.Src = path.Base(p.Src)
- }
- dt, err = json.Marshal(p)
- if err != nil {
- return nil, false, err
- }
- }
- actions = append(actions, dt)
- indexes = append(indexes, []int{int(action.Input), int(action.SecondaryInput), int(action.Output)})
- }
- if isDefaultIndexes(indexes) {
- indexes = nil
- }
- dt, err := json.Marshal(struct {
- Type string
- Actions [][]byte
- Indexes [][]int `json:"indexes,omitempty"`
- }{
- Type: fileCacheType,
- Actions: actions,
- Indexes: indexes,
- })
- if err != nil {
- return nil, false, err
- }
- cm := &solver.CacheMap{
- Digest: digest.FromBytes(dt),
- Deps: make([]struct {
- Selector digest.Digest
- ComputeDigestFunc solver.ResultBasedCacheFunc
- PreprocessFunc solver.PreprocessFunc
- }, f.numInputs),
- }
- 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))
- }
- sort.Slice(dgsts, func(i, j int) bool {
- return bytes.Compare(dgsts[i], dgsts[j]) > 0
- })
- cm.Deps[idx].Selector = digest.FromBytes(bytes.Join(dgsts, []byte{0}))
- cm.Deps[idx].ComputeDigestFunc = llbsolver.NewContentHashFunc(dedupeSelectors(m))
- }
- for idx := range cm.Deps {
- cm.Deps[idx].PreprocessFunc = llbsolver.UnlazyResultFunc
- }
- return cm, true, nil
- }
- func (f *fileOp) Exec(ctx context.Context, g session.Group, inputs []solver.Result) ([]solver.Result, error) {
- inpRefs := make([]fileoptypes.Ref, 0, len(inputs))
- for _, inp := range inputs {
- workerRef, ok := inp.Sys().(*worker.WorkerRef)
- if !ok {
- return nil, errors.Errorf("invalid reference for exec %T", inp.Sys())
- }
- inpRefs = append(inpRefs, workerRef.ImmutableRef)
- }
- outs, err := f.solver.Solve(ctx, inpRefs, f.op.Actions, g)
- if err != nil {
- return nil, err
- }
- outResults := make([]solver.Result, 0, len(outs))
- for _, out := range outs {
- outResults = append(outResults, worker.NewWorkerRefResult(out.(cache.ImmutableRef), f.w))
- }
- return outResults, nil
- }
- func addSelector(m map[int]map[llbsolver.Selector]struct{}, idx int, sel string, wildcard, followLinks bool) {
- mm, ok := m[idx]
- if !ok {
- mm = map[llbsolver.Selector]struct{}{}
- m[idx] = mm
- }
- s := llbsolver.Selector{Path: sel}
- if wildcard && containsWildcards(sel) {
- s.Wildcard = true
- }
- if followLinks {
- s.FollowLinks = true
- }
- mm[s] = struct{}{}
- }
- func containsWildcards(name string) bool {
- isWindows := runtime.GOOS == "windows"
- for i := 0; i < len(name); i++ {
- ch := name[i]
- if ch == '\\' && !isWindows {
- i++
- } else if ch == '*' || ch == '?' || ch == '[' {
- return true
- }
- }
- return false
- }
- func dedupeSelectors(m map[llbsolver.Selector]struct{}) []llbsolver.Selector {
- paths := make([]string, 0, len(m))
- pathsFollow := make([]string, 0, len(m))
- for sel := range m {
- if !sel.Wildcard {
- if sel.FollowLinks {
- pathsFollow = append(pathsFollow, sel.Path)
- } else {
- paths = append(paths, sel.Path)
- }
- }
- }
- paths = dedupePaths(paths)
- pathsFollow = dedupePaths(pathsFollow)
- selectors := make([]llbsolver.Selector, 0, len(m))
- for _, p := range paths {
- selectors = append(selectors, llbsolver.Selector{Path: p})
- }
- for _, p := range pathsFollow {
- selectors = append(selectors, llbsolver.Selector{Path: p, FollowLinks: true})
- }
- for sel := range m {
- if sel.Wildcard {
- selectors = append(selectors, sel)
- }
- }
- sort.Slice(selectors, func(i, j int) bool {
- return selectors[i].Path < selectors[j].Path
- })
- return selectors
- }
- func processOwner(chopt *pb.ChownOpt, selectors map[int]map[llbsolver.Selector]struct{}) error {
- if chopt == nil {
- return nil
- }
- if chopt.User != nil {
- if u, ok := chopt.User.User.(*pb.UserOpt_ByName); ok {
- if u.ByName.Input < 0 {
- return errors.Errorf("invalid user index %d", u.ByName.Input)
- }
- addSelector(selectors, int(u.ByName.Input), "/etc/passwd", false, true)
- }
- }
- if chopt.Group != nil {
- if u, ok := chopt.Group.User.(*pb.UserOpt_ByName); ok {
- if u.ByName.Input < 0 {
- return errors.Errorf("invalid user index %d", u.ByName.Input)
- }
- addSelector(selectors, int(u.ByName.Input), "/etc/group", false, true)
- }
- }
- return nil
- }
- func NewFileOpSolver(w worker.Worker, b fileoptypes.Backend, r fileoptypes.RefManager) *FileOpSolver {
- return &FileOpSolver{
- w: w,
- b: b,
- r: r,
- outs: map[int]int{},
- ins: map[int]input{},
- }
- }
- type FileOpSolver struct {
- w worker.Worker
- b fileoptypes.Backend
- r fileoptypes.RefManager
- mu sync.Mutex
- outs map[int]int
- ins map[int]input
- g flightcontrol.Group
- }
- type input struct {
- requiresCommit bool
- mount fileoptypes.Mount
- ref fileoptypes.Ref
- }
- func (s *FileOpSolver) Solve(ctx context.Context, inputs []fileoptypes.Ref, actions []*pb.FileAction, g session.Group) ([]fileoptypes.Ref, error) {
- for i, a := range actions {
- if int(a.Input) < -1 || int(a.Input) >= len(inputs)+len(actions) {
- return nil, errors.Errorf("invalid input index %d, %d provided", a.Input, len(inputs)+len(actions))
- }
- if int(a.SecondaryInput) < -1 || int(a.SecondaryInput) >= len(inputs)+len(actions) {
- return nil, errors.Errorf("invalid secondary input index %d, %d provided", a.Input, len(inputs))
- }
- inp, ok := s.ins[int(a.Input)]
- if ok {
- inp.requiresCommit = true
- }
- s.ins[int(a.Input)] = inp
- inp, ok = s.ins[int(a.SecondaryInput)]
- if ok {
- inp.requiresCommit = true
- }
- s.ins[int(a.SecondaryInput)] = inp
- if a.Output != -1 {
- if _, ok := s.outs[int(a.Output)]; ok {
- return nil, errors.Errorf("duplicate output %d", a.Output)
- }
- idx := len(inputs) + i
- s.outs[int(a.Output)] = idx
- s.ins[idx] = input{requiresCommit: true}
- }
- }
- if len(s.outs) == 0 {
- return nil, errors.Errorf("no outputs specified")
- }
- for i := 0; i < len(s.outs); i++ {
- if _, ok := s.outs[i]; !ok {
- return nil, errors.Errorf("missing output index %d", i)
- }
- }
- defer func() {
- for _, in := range s.ins {
- if in.ref == nil && in.mount != nil {
- in.mount.Release(context.TODO())
- }
- }
- }()
- outs := make([]fileoptypes.Ref, len(s.outs))
- eg, ctx := errgroup.WithContext(ctx)
- for i, idx := range s.outs {
- func(i, idx int) {
- eg.Go(func() error {
- if err := s.validate(idx, inputs, actions, nil); err != nil {
- return err
- }
- inp, err := s.getInput(ctx, idx, inputs, actions, g)
- if err != nil {
- return errdefs.WithFileActionError(err, idx-len(inputs))
- }
- outs[i] = inp.ref
- return nil
- })
- }(i, idx)
- }
- if err := eg.Wait(); err != nil {
- for _, r := range outs {
- if r != nil {
- r.Release(context.TODO())
- }
- }
- return nil, err
- }
- return outs, nil
- }
- func (s *FileOpSolver) validate(idx int, inputs []fileoptypes.Ref, actions []*pb.FileAction, loaded []int) error {
- for _, check := range loaded {
- if idx == check {
- return errors.Errorf("loop from index %d", idx)
- }
- }
- if idx < len(inputs) {
- return nil
- }
- loaded = append(loaded, idx)
- action := actions[idx-len(inputs)]
- for _, inp := range []int{int(action.Input), int(action.SecondaryInput)} {
- if err := s.validate(inp, inputs, actions, loaded); err != nil {
- return err
- }
- }
- return nil
- }
- func (s *FileOpSolver) getInput(ctx context.Context, idx int, inputs []fileoptypes.Ref, actions []*pb.FileAction, g session.Group) (input, error) {
- inp, err := s.g.Do(ctx, fmt.Sprintf("inp-%d", idx), func(ctx context.Context) (_ interface{}, err error) {
- s.mu.Lock()
- inp := s.ins[idx]
- s.mu.Unlock()
- if inp.mount != nil || inp.ref != nil {
- return inp, nil
- }
- if idx < len(inputs) {
- inp.ref = inputs[idx]
- s.mu.Lock()
- s.ins[idx] = inp
- s.mu.Unlock()
- return inp, nil
- }
- var inpMount, inpMountSecondary fileoptypes.Mount
- var toRelease []fileoptypes.Mount
- action := actions[idx-len(inputs)]
- defer func() {
- if err != nil && inpMount != nil {
- inputRes := make([]solver.Result, len(inputs))
- for i, input := range inputs {
- inputRes[i] = worker.NewWorkerRefResult(input.(cache.ImmutableRef), s.w)
- }
- outputRes := make([]solver.Result, len(actions))
- // Commit the mutable for the primary input of the failed action.
- if !inpMount.Readonly() {
- ref, cerr := s.r.Commit(ctx, inpMount)
- if cerr == nil {
- outputRes[idx-len(inputs)] = worker.NewWorkerRefResult(ref.(cache.ImmutableRef), s.w)
- }
- }
- // If the action has a secondary input, commit it and set the ref on
- // the output results.
- if inpMountSecondary != nil && !inpMountSecondary.Readonly() {
- ref2, cerr := s.r.Commit(ctx, inpMountSecondary)
- if cerr == nil {
- outputRes[int(action.SecondaryInput)-len(inputs)] = worker.NewWorkerRefResult(ref2.(cache.ImmutableRef), s.w)
- }
- }
- err = errdefs.WithExecError(err, inputRes, outputRes)
- }
- for _, m := range toRelease {
- m.Release(context.TODO())
- }
- }()
- loadInput := func(ctx context.Context) func() error {
- return func() error {
- inp, err := s.getInput(ctx, int(action.Input), inputs, actions, g)
- if err != nil {
- return err
- }
- if inp.ref != nil {
- m, err := s.r.Prepare(ctx, inp.ref, false, g)
- if err != nil {
- return err
- }
- inpMount = m
- return nil
- }
- inpMount = inp.mount
- return nil
- }
- }
- loadSecondaryInput := func(ctx context.Context) func() error {
- return func() error {
- inp, err := s.getInput(ctx, int(action.SecondaryInput), inputs, actions, g)
- if err != nil {
- return err
- }
- if inp.ref != nil {
- m, err := s.r.Prepare(ctx, inp.ref, true, g)
- if err != nil {
- return err
- }
- inpMountSecondary = m
- toRelease = append(toRelease, m)
- return nil
- }
- inpMountSecondary = inp.mount
- return nil
- }
- }
- loadUser := func(ctx context.Context, uopt *pb.UserOpt) (fileoptypes.Mount, error) {
- if uopt == nil {
- return nil, nil
- }
- switch u := uopt.User.(type) {
- case *pb.UserOpt_ByName:
- var m fileoptypes.Mount
- if u.ByName.Input < 0 {
- return nil, errors.Errorf("invalid user index: %d", u.ByName.Input)
- }
- inp, err := s.getInput(ctx, int(u.ByName.Input), inputs, actions, g)
- if err != nil {
- return nil, err
- }
- if inp.ref != nil {
- mm, err := s.r.Prepare(ctx, inp.ref, true, g)
- if err != nil {
- return nil, err
- }
- toRelease = append(toRelease, mm)
- m = mm
- } else {
- m = inp.mount
- }
- return m, nil
- default:
- return nil, nil
- }
- }
- loadOwner := func(ctx context.Context, chopt *pb.ChownOpt) (fileoptypes.Mount, fileoptypes.Mount, error) {
- if chopt == nil {
- return nil, nil, nil
- }
- um, err := loadUser(ctx, chopt.User)
- if err != nil {
- return nil, nil, err
- }
- gm, err := loadUser(ctx, chopt.Group)
- if err != nil {
- return nil, nil, err
- }
- return um, gm, nil
- }
- if action.Input != -1 && action.SecondaryInput != -1 {
- eg, ctx := errgroup.WithContext(ctx)
- eg.Go(loadInput(ctx))
- eg.Go(loadSecondaryInput(ctx))
- if err := eg.Wait(); err != nil {
- return nil, err
- }
- } else {
- if action.Input != -1 {
- if err := loadInput(ctx)(); err != nil {
- return nil, err
- }
- }
- if action.SecondaryInput != -1 {
- if err := loadSecondaryInput(ctx)(); err != nil {
- return nil, err
- }
- }
- }
- if inpMount == nil {
- m, err := s.r.Prepare(ctx, nil, false, g)
- if err != nil {
- return nil, err
- }
- inpMount = m
- }
- switch a := action.Action.(type) {
- case *pb.FileAction_Mkdir:
- user, group, err := loadOwner(ctx, a.Mkdir.Owner)
- if err != nil {
- return nil, err
- }
- if err := s.b.Mkdir(ctx, inpMount, user, group, *a.Mkdir); err != nil {
- return nil, err
- }
- case *pb.FileAction_Mkfile:
- user, group, err := loadOwner(ctx, a.Mkfile.Owner)
- if err != nil {
- return nil, err
- }
- if err := s.b.Mkfile(ctx, inpMount, user, group, *a.Mkfile); err != nil {
- return nil, err
- }
- case *pb.FileAction_Rm:
- if err := s.b.Rm(ctx, inpMount, *a.Rm); err != nil {
- return nil, err
- }
- case *pb.FileAction_Copy:
- if inpMountSecondary == nil {
- m, err := s.r.Prepare(ctx, nil, true, g)
- if err != nil {
- return nil, err
- }
- inpMountSecondary = m
- }
- user, group, err := loadOwner(ctx, a.Copy.Owner)
- if err != nil {
- return nil, err
- }
- if err := s.b.Copy(ctx, inpMountSecondary, inpMount, user, group, *a.Copy); err != nil {
- return nil, err
- }
- default:
- return nil, errors.Errorf("invalid action type %T", action.Action)
- }
- if inp.requiresCommit {
- ref, err := s.r.Commit(ctx, inpMount)
- if err != nil {
- return nil, err
- }
- inp.ref = ref
- } else {
- inp.mount = inpMount
- }
- s.mu.Lock()
- s.ins[idx] = inp
- s.mu.Unlock()
- return inp, nil
- })
- if err != nil {
- return input{}, err
- }
- return inp.(input), err
- }
- func isDefaultIndexes(idxs [][]int) bool {
- // Older version of checksum did not contain indexes for actions resulting in possibility for a wrong cache match.
- // We detect the most common pattern for indexes and maintain old checksum for that case to minimize cache misses on upgrade.
- // If a future change causes braking changes in instruction cache consider removing this exception.
- if len(idxs) == 0 {
- return false
- }
- for i, idx := range idxs {
- if len(idx) != 3 {
- return false
- }
- // input for first action is first input
- if i == 0 && idx[0] != 0 {
- return false
- }
- // input for other actions is previous action
- if i != 0 && idx[0] != len(idxs)+(i-1) {
- return false
- }
- // secondary input is second input or -1
- if idx[1] != -1 && idx[1] != 1 {
- return false
- }
- // last action creates output
- if i == len(idxs)-1 && idx[2] != 0 {
- return false
- }
- // other actions do not create an output
- if i != len(idxs)-1 && idx[2] != -1 {
- return false
- }
- }
- return true
- }
|