| package llb |
| |
| import ( |
| "context" |
| _ "crypto/sha256" // for opencontainers/go-digest |
| "os" |
| "path" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/moby/buildkit/solver/pb" |
| digest "github.com/opencontainers/go-digest" |
| "github.com/pkg/errors" |
| ) |
| |
| // Examples: |
| // local := llb.Local(...) |
| // llb.Image().Dir("/abc").File(Mkdir("./foo").Mkfile("/abc/foo/bar", []byte("data"))) |
| // llb.Image().File(Mkdir("/foo").Mkfile("/foo/bar", []byte("data"))) |
| // llb.Image().File(Copy(local, "/foo", "/bar")).File(Copy(local, "/foo2", "/bar2")) |
| // |
| // a := Mkdir("./foo") // *FileAction /ced/foo |
| // b := Mkdir("./bar") // /abc/bar |
| // c := b.Copy(a.WithState(llb.Scratch().Dir("/ced")), "./foo", "./baz") // /abc/baz |
| // llb.Image().Dir("/abc").File(c) |
| // |
| // In future this can be extended to multiple outputs with: |
| // a := Mkdir("./foo") |
| // b, id := a.GetSelector() |
| // c := b.Mkdir("./bar") |
| // filestate = state.File(c) |
| // filestate.GetOutput(id).Exec() |
| |
| func NewFileOp(s State, action *FileAction, c Constraints) *FileOp { |
| action = action.bind(s) |
| |
| f := &FileOp{ |
| action: action, |
| constraints: c, |
| } |
| |
| f.output = &output{vertex: f, getIndex: func() (pb.OutputIndex, error) { |
| return pb.OutputIndex(0), nil |
| }} |
| |
| return f |
| } |
| |
| // CopyInput is either llb.State or *FileActionWithState |
| type CopyInput interface { |
| isFileOpCopyInput() |
| } |
| |
| type subAction interface { |
| toProtoAction(context.Context, string, pb.InputIndex) (pb.IsFileAction, error) |
| } |
| |
| type FileAction struct { |
| state *State |
| prev *FileAction |
| action subAction |
| err error |
| } |
| |
| func (fa *FileAction) Mkdir(p string, m os.FileMode, opt ...MkdirOption) *FileAction { |
| a := Mkdir(p, m, opt...) |
| a.prev = fa |
| return a |
| } |
| |
| func (fa *FileAction) Mkfile(p string, m os.FileMode, dt []byte, opt ...MkfileOption) *FileAction { |
| a := Mkfile(p, m, dt, opt...) |
| a.prev = fa |
| return a |
| } |
| |
| func (fa *FileAction) Rm(p string, opt ...RmOption) *FileAction { |
| a := Rm(p, opt...) |
| a.prev = fa |
| return a |
| } |
| |
| func (fa *FileAction) Copy(input CopyInput, src, dest string, opt ...CopyOption) *FileAction { |
| a := Copy(input, src, dest, opt...) |
| a.prev = fa |
| return a |
| } |
| |
| func (fa *FileAction) allOutputs(m map[Output]struct{}) { |
| if fa == nil { |
| return |
| } |
| if fa.state != nil && fa.state.Output() != nil { |
| m[fa.state.Output()] = struct{}{} |
| } |
| |
| if a, ok := fa.action.(*fileActionCopy); ok { |
| if a.state != nil { |
| if out := a.state.Output(); out != nil { |
| m[out] = struct{}{} |
| } |
| } else if a.fas != nil { |
| a.fas.allOutputs(m) |
| } |
| } |
| fa.prev.allOutputs(m) |
| } |
| |
| func (fa *FileAction) bind(s State) *FileAction { |
| if fa == nil { |
| return nil |
| } |
| fa2 := *fa |
| fa2.prev = fa.prev.bind(s) |
| fa2.state = &s |
| return &fa2 |
| } |
| |
| func (fa *FileAction) WithState(s State) CopyInput { |
| return &fileActionWithState{FileAction: fa.bind(s)} |
| } |
| |
| type fileActionWithState struct { |
| *FileAction |
| } |
| |
| func (fas *fileActionWithState) isFileOpCopyInput() {} |
| |
| func Mkdir(p string, m os.FileMode, opt ...MkdirOption) *FileAction { |
| var mi MkdirInfo |
| for _, o := range opt { |
| o.SetMkdirOption(&mi) |
| } |
| return &FileAction{ |
| action: &fileActionMkdir{ |
| file: p, |
| mode: m, |
| info: mi, |
| }, |
| } |
| } |
| |
| type fileActionMkdir struct { |
| file string |
| mode os.FileMode |
| info MkdirInfo |
| } |
| |
| func (a *fileActionMkdir) toProtoAction(ctx context.Context, parent string, base pb.InputIndex) (pb.IsFileAction, error) { |
| return &pb.FileAction_Mkdir{ |
| Mkdir: &pb.FileActionMkDir{ |
| Path: normalizePath(parent, a.file, false), |
| Mode: int32(a.mode & 0777), |
| MakeParents: a.info.MakeParents, |
| Owner: a.info.ChownOpt.marshal(base), |
| Timestamp: marshalTime(a.info.CreatedTime), |
| }, |
| }, nil |
| } |
| |
| type MkdirOption interface { |
| SetMkdirOption(*MkdirInfo) |
| } |
| |
| type ChownOption interface { |
| MkdirOption |
| MkfileOption |
| CopyOption |
| } |
| |
| type mkdirOptionFunc func(*MkdirInfo) |
| |
| func (fn mkdirOptionFunc) SetMkdirOption(mi *MkdirInfo) { |
| fn(mi) |
| } |
| |
| var _ MkdirOption = &MkdirInfo{} |
| |
| func WithParents(b bool) MkdirOption { |
| return mkdirOptionFunc(func(mi *MkdirInfo) { |
| mi.MakeParents = b |
| }) |
| } |
| |
| type MkdirInfo struct { |
| MakeParents bool |
| ChownOpt *ChownOpt |
| CreatedTime *time.Time |
| } |
| |
| func (mi *MkdirInfo) SetMkdirOption(mi2 *MkdirInfo) { |
| *mi2 = *mi |
| } |
| |
| func WithUser(name string) ChownOption { |
| 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 { |
| return ChownOpt{ |
| User: &UserOpt{UID: uid}, |
| Group: &UserOpt{UID: gid}, |
| } |
| } |
| |
| type ChownOpt struct { |
| User *UserOpt |
| Group *UserOpt |
| } |
| |
| func (co ChownOpt) SetMkdirOption(mi *MkdirInfo) { |
| mi.ChownOpt = &co |
| } |
| func (co ChownOpt) SetMkfileOption(mi *MkfileInfo) { |
| mi.ChownOpt = &co |
| } |
| func (co ChownOpt) SetCopyOption(mi *CopyInfo) { |
| mi.ChownOpt = &co |
| } |
| |
| func (co *ChownOpt) marshal(base pb.InputIndex) *pb.ChownOpt { |
| if co == nil { |
| return nil |
| } |
| return &pb.ChownOpt{ |
| User: co.User.marshal(base), |
| Group: co.Group.marshal(base), |
| } |
| } |
| |
| type UserOpt struct { |
| UID int |
| Name string |
| } |
| |
| func (up *UserOpt) marshal(base pb.InputIndex) *pb.UserOpt { |
| if up == nil { |
| return nil |
| } |
| if up.Name != "" { |
| return &pb.UserOpt{User: &pb.UserOpt_ByName{ByName: &pb.NamedUserOpt{ |
| Name: up.Name, Input: base}}} |
| } |
| return &pb.UserOpt{User: &pb.UserOpt_ByID{ByID: uint32(up.UID)}} |
| } |
| |
| func Mkfile(p string, m os.FileMode, dt []byte, opts ...MkfileOption) *FileAction { |
| var mi MkfileInfo |
| for _, o := range opts { |
| o.SetMkfileOption(&mi) |
| } |
| |
| return &FileAction{ |
| action: &fileActionMkfile{ |
| file: p, |
| mode: m, |
| dt: dt, |
| info: mi, |
| }, |
| } |
| } |
| |
| type MkfileOption interface { |
| SetMkfileOption(*MkfileInfo) |
| } |
| |
| type MkfileInfo struct { |
| ChownOpt *ChownOpt |
| CreatedTime *time.Time |
| } |
| |
| func (mi *MkfileInfo) SetMkfileOption(mi2 *MkfileInfo) { |
| *mi2 = *mi |
| } |
| |
| var _ MkfileOption = &MkfileInfo{} |
| |
| type fileActionMkfile struct { |
| file string |
| mode os.FileMode |
| dt []byte |
| info MkfileInfo |
| } |
| |
| func (a *fileActionMkfile) toProtoAction(ctx context.Context, parent string, base pb.InputIndex) (pb.IsFileAction, error) { |
| return &pb.FileAction_Mkfile{ |
| Mkfile: &pb.FileActionMkFile{ |
| Path: normalizePath(parent, a.file, false), |
| Mode: int32(a.mode & 0777), |
| Data: a.dt, |
| Owner: a.info.ChownOpt.marshal(base), |
| Timestamp: marshalTime(a.info.CreatedTime), |
| }, |
| }, nil |
| } |
| |
| func Rm(p string, opts ...RmOption) *FileAction { |
| var mi RmInfo |
| for _, o := range opts { |
| o.SetRmOption(&mi) |
| } |
| |
| return &FileAction{ |
| action: &fileActionRm{ |
| file: p, |
| info: mi, |
| }, |
| } |
| } |
| |
| type RmOption interface { |
| SetRmOption(*RmInfo) |
| } |
| |
| type rmOptionFunc func(*RmInfo) |
| |
| func (fn rmOptionFunc) SetRmOption(mi *RmInfo) { |
| fn(mi) |
| } |
| |
| type RmInfo struct { |
| AllowNotFound bool |
| AllowWildcard bool |
| } |
| |
| func (mi *RmInfo) SetRmOption(mi2 *RmInfo) { |
| *mi2 = *mi |
| } |
| |
| var _ RmOption = &RmInfo{} |
| |
| func WithAllowNotFound(b bool) RmOption { |
| return rmOptionFunc(func(mi *RmInfo) { |
| mi.AllowNotFound = b |
| }) |
| } |
| |
| func WithAllowWildcard(b bool) RmOption { |
| return rmOptionFunc(func(mi *RmInfo) { |
| mi.AllowWildcard = b |
| }) |
| } |
| |
| type fileActionRm struct { |
| file string |
| info RmInfo |
| } |
| |
| func (a *fileActionRm) toProtoAction(ctx context.Context, parent string, base pb.InputIndex) (pb.IsFileAction, error) { |
| return &pb.FileAction_Rm{ |
| Rm: &pb.FileActionRm{ |
| Path: normalizePath(parent, a.file, false), |
| AllowNotFound: a.info.AllowNotFound, |
| AllowWildcard: a.info.AllowWildcard, |
| }, |
| }, nil |
| } |
| |
| func Copy(input CopyInput, src, dest string, opts ...CopyOption) *FileAction { |
| var state *State |
| var fas *fileActionWithState |
| var err error |
| if st, ok := input.(State); ok { |
| state = &st |
| } else if v, ok := input.(*fileActionWithState); ok { |
| fas = v |
| } else { |
| err = errors.Errorf("invalid input type %T for copy", input) |
| } |
| |
| var mi CopyInfo |
| for _, o := range opts { |
| o.SetCopyOption(&mi) |
| } |
| |
| return &FileAction{ |
| action: &fileActionCopy{ |
| state: state, |
| fas: fas, |
| src: src, |
| dest: dest, |
| info: mi, |
| }, |
| err: err, |
| } |
| } |
| |
| type CopyOption interface { |
| SetCopyOption(*CopyInfo) |
| } |
| |
| type CopyInfo struct { |
| Mode *os.FileMode |
| FollowSymlinks bool |
| CopyDirContentsOnly bool |
| AttemptUnpack bool |
| CreateDestPath bool |
| AllowWildcard bool |
| AllowEmptyWildcard bool |
| ChownOpt *ChownOpt |
| CreatedTime *time.Time |
| } |
| |
| func (mi *CopyInfo) SetCopyOption(mi2 *CopyInfo) { |
| *mi2 = *mi |
| } |
| |
| var _ CopyOption = &CopyInfo{} |
| |
| type fileActionCopy struct { |
| state *State |
| fas *fileActionWithState |
| src string |
| dest string |
| info CopyInfo |
| } |
| |
| func (a *fileActionCopy) toProtoAction(ctx context.Context, parent string, base pb.InputIndex) (pb.IsFileAction, error) { |
| src, err := a.sourcePath(ctx) |
| if err != nil { |
| return nil, err |
| } |
| c := &pb.FileActionCopy{ |
| Src: src, |
| Dest: normalizePath(parent, a.dest, true), |
| Owner: a.info.ChownOpt.marshal(base), |
| AllowWildcard: a.info.AllowWildcard, |
| AllowEmptyWildcard: a.info.AllowEmptyWildcard, |
| FollowSymlink: a.info.FollowSymlinks, |
| DirCopyContents: a.info.CopyDirContentsOnly, |
| AttemptUnpackDockerCompatibility: a.info.AttemptUnpack, |
| CreateDestPath: a.info.CreateDestPath, |
| Timestamp: marshalTime(a.info.CreatedTime), |
| } |
| if a.info.Mode != nil { |
| c.Mode = int32(*a.info.Mode) |
| } else { |
| c.Mode = -1 |
| } |
| return &pb.FileAction_Copy{ |
| Copy: c, |
| }, nil |
| } |
| |
| func (a *fileActionCopy) sourcePath(ctx context.Context) (string, error) { |
| p := path.Clean(a.src) |
| if !path.IsAbs(p) { |
| if a.state != nil { |
| dir, err := a.state.GetDir(ctx) |
| if err != nil { |
| return "", err |
| } |
| p = path.Join("/", dir, p) |
| } else if a.fas != nil { |
| dir, err := a.fas.state.GetDir(ctx) |
| if err != nil { |
| return "", err |
| } |
| p = path.Join("/", dir, p) |
| } |
| } |
| return p, nil |
| } |
| |
| type CreatedTime time.Time |
| |
| func WithCreatedTime(t time.Time) CreatedTime { |
| return CreatedTime(t) |
| } |
| |
| func (c CreatedTime) SetMkdirOption(mi *MkdirInfo) { |
| mi.CreatedTime = (*time.Time)(&c) |
| } |
| |
| func (c CreatedTime) SetMkfileOption(mi *MkfileInfo) { |
| mi.CreatedTime = (*time.Time)(&c) |
| } |
| |
| func (c CreatedTime) SetCopyOption(mi *CopyInfo) { |
| mi.CreatedTime = (*time.Time)(&c) |
| } |
| |
| func marshalTime(t *time.Time) int64 { |
| if t == nil { |
| return -1 |
| } |
| return t.UnixNano() |
| } |
| |
| type FileOp struct { |
| MarshalCache |
| action *FileAction |
| output Output |
| |
| constraints Constraints |
| isValidated bool |
| } |
| |
| func (f *FileOp) Validate(context.Context) error { |
| if f.isValidated { |
| return nil |
| } |
| if f.action == nil { |
| return errors.Errorf("action is required") |
| } |
| f.isValidated = true |
| return nil |
| } |
| |
| type marshalState struct { |
| ctx context.Context |
| visited map[*FileAction]*fileActionState |
| inputs []*pb.Input |
| actions []*fileActionState |
| } |
| |
| func newMarshalState(ctx context.Context) *marshalState { |
| return &marshalState{ |
| visited: map[*FileAction]*fileActionState{}, |
| ctx: ctx, |
| } |
| } |
| |
| type fileActionState struct { |
| base pb.InputIndex |
| input pb.InputIndex |
| inputRelative *int |
| input2 pb.InputIndex |
| input2Relative *int |
| target int |
| action subAction |
| fa *FileAction |
| } |
| |
| func (ms *marshalState) addInput(st *fileActionState, c *Constraints, o Output) (pb.InputIndex, error) { |
| inp, err := o.ToInput(ms.ctx, c) |
| if err != nil { |
| return 0, err |
| } |
| for i, inp2 := range ms.inputs { |
| if *inp == *inp2 { |
| return pb.InputIndex(i), nil |
| } |
| } |
| i := pb.InputIndex(len(ms.inputs)) |
| ms.inputs = append(ms.inputs, inp) |
| return i, nil |
| } |
| |
| func (ms *marshalState) add(fa *FileAction, c *Constraints) (*fileActionState, error) { |
| if st, ok := ms.visited[fa]; ok { |
| return st, nil |
| } |
| |
| if fa.err != nil { |
| return nil, fa.err |
| } |
| |
| var prevState *fileActionState |
| if parent := fa.prev; parent != nil { |
| var err error |
| prevState, err = ms.add(parent, c) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| st := &fileActionState{ |
| action: fa.action, |
| input: -1, |
| input2: -1, |
| base: -1, |
| fa: fa, |
| } |
| |
| if source := fa.state.Output(); source != nil { |
| inp, err := ms.addInput(st, c, source) |
| if err != nil { |
| return nil, err |
| } |
| st.base = inp |
| } |
| |
| if fa.prev == nil { |
| st.input = st.base |
| } else { |
| st.inputRelative = &prevState.target |
| } |
| |
| if a, ok := fa.action.(*fileActionCopy); ok { |
| if a.state != nil { |
| if out := a.state.Output(); out != nil { |
| inp, err := ms.addInput(st, c, out) |
| if err != nil { |
| return nil, err |
| } |
| st.input2 = inp |
| } |
| } else if a.fas != nil { |
| src, err := ms.add(a.fas.FileAction, c) |
| if err != nil { |
| return nil, err |
| } |
| st.input2Relative = &src.target |
| } else { |
| return nil, errors.Errorf("invalid empty source for copy") |
| } |
| } |
| |
| st.target = len(ms.actions) |
| |
| ms.visited[fa] = st |
| ms.actions = append(ms.actions, st) |
| |
| return st, nil |
| } |
| |
| func (f *FileOp) Marshal(ctx context.Context, c *Constraints) (digest.Digest, []byte, *pb.OpMetadata, []*SourceLocation, error) { |
| if f.Cached(c) { |
| return f.Load() |
| } |
| if err := f.Validate(ctx); err != nil { |
| return "", nil, nil, nil, err |
| } |
| |
| addCap(&f.constraints, pb.CapFileBase) |
| |
| pfo := &pb.FileOp{} |
| |
| if f.constraints.Platform == nil { |
| p, err := getPlatform(*f.action.state)(ctx) |
| if err != nil { |
| return "", nil, nil, nil, err |
| } |
| f.constraints.Platform = p |
| } |
| |
| pop, md := MarshalConstraints(c, &f.constraints) |
| pop.Op = &pb.Op_File{ |
| File: pfo, |
| } |
| |
| state := newMarshalState(ctx) |
| _, err := state.add(f.action, c) |
| if err != nil { |
| return "", nil, nil, nil, err |
| } |
| pop.Inputs = state.inputs |
| |
| for i, st := range state.actions { |
| output := pb.OutputIndex(-1) |
| if i+1 == len(state.actions) { |
| output = 0 |
| } |
| |
| var parent string |
| if st.fa.state != nil { |
| parent, err = st.fa.state.GetDir(ctx) |
| if err != nil { |
| return "", nil, nil, nil, err |
| } |
| } |
| |
| action, err := st.action.toProtoAction(ctx, parent, st.base) |
| if err != nil { |
| return "", nil, nil, nil, err |
| } |
| |
| pfo.Actions = append(pfo.Actions, &pb.FileAction{ |
| Input: getIndex(st.input, len(state.inputs), st.inputRelative), |
| SecondaryInput: getIndex(st.input2, len(state.inputs), st.input2Relative), |
| Output: output, |
| Action: action, |
| }) |
| } |
| |
| dt, err := pop.Marshal() |
| if err != nil { |
| return "", nil, nil, nil, err |
| } |
| f.Store(dt, md, f.constraints.SourceLocations, c) |
| return f.Load() |
| } |
| |
| func normalizePath(parent, p string, keepSlash bool) string { |
| origPath := p |
| p = path.Clean(p) |
| if !path.IsAbs(p) { |
| p = path.Join("/", parent, p) |
| } |
| if keepSlash { |
| if strings.HasSuffix(origPath, "/") && !strings.HasSuffix(p, "/") { |
| p += "/" |
| } else if strings.HasSuffix(origPath, "/.") { |
| if p != "/" { |
| p += "/" |
| } |
| p += "." |
| } |
| } |
| return p |
| } |
| |
| func (f *FileOp) Output() Output { |
| return f.output |
| } |
| |
| func (f *FileOp) Inputs() (inputs []Output) { |
| mm := map[Output]struct{}{} |
| |
| f.action.allOutputs(mm) |
| |
| for o := range mm { |
| inputs = append(inputs, o) |
| } |
| return inputs |
| } |
| |
| func getIndex(input pb.InputIndex, len int, relative *int) pb.InputIndex { |
| if relative != nil { |
| return pb.InputIndex(len + *relative) |
| } |
| return input |
| } |