Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FileActionSymlink and llb.Symlink #5519

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testLayerLimitOnMounts,
testFrontendVerifyPlatforms,
testRunValidExitCodes,
testFileOpSymlink,
}

func TestIntegration(t *testing.T) {
Expand Down Expand Up @@ -2409,6 +2410,90 @@ func testOCILayoutPlatformSource(t *testing.T, sb integration.Sandbox) {
}
}

func testFileOpSymlink(t *testing.T, sb integration.Sandbox) {
requiresLinux(t)

const (
fileOwner = 7777
fileGroup = 8888
linkOwner = 1111
linkGroup = 2222

dummyTimestamp = 42
)

dummyTime := time.Unix(dummyTimestamp, 0)

c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

st := llb.Scratch().
File(llb.Mkdir("/foo", 0700).Mkfile("bar", 0600, []byte("contents"), llb.ChownOpt{
User: &llb.UserOpt{
UID: fileOwner,
},
Group: &llb.UserOpt{
UID: fileGroup,
},
})).
File(llb.Symlink("bar", "/baz", llb.WithCreatedTime(dummyTime), llb.ChownOpt{
User: &llb.UserOpt{
UID: linkOwner,
},
Group: &llb.UserOpt{
UID: linkGroup,
},
}))

def, err := st.Marshal(sb.Context())
require.NoError(t, err)

destDir := t.TempDir()

out := filepath.Join(destDir, "out.tar")
outW, err := os.Create(out)
require.NoError(t, err)
defer outW.Close()

_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterTar,
Output: fixedWriteCloser(outW),
},
},
}, nil)
require.NoError(t, err)

dt, err := os.ReadFile(out)
require.NoError(t, err)
m, err := testutil.ReadTarToMap(dt, false)
require.NoError(t, err)

entry, ok := m["bar"]
require.True(t, ok)

dt = entry.Data
header := entry.Header
require.NoError(t, err)

require.Equal(t, []byte("contents"), dt)
require.Equal(t, fileOwner, header.Uid)
require.Equal(t, fileGroup, header.Gid)

entry, ok = m["baz"]
header = entry.Header
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this check that it is a symlink and that it points to the correct location?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted, will fix this by checking header.Linkname and verifying its destination.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

require.NoError(t, err)

// make sure it was chowned properly
require.Equal(t, true, ok)
require.Equal(t, linkOwner, header.Uid)
require.Equal(t, linkGroup, header.Gid)

require.Equal(t, dummyTime, header.ModTime)
}

func testFileOpRmWildcard(t *testing.T, sb integration.Sandbox) {
requiresLinux(t)
c, err := New(sb.Context(), sb.Address())
Expand Down
67 changes: 67 additions & 0 deletions client/llb/fileop.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ func (fa *FileAction) Mkfile(p string, m os.FileMode, dt []byte, opt ...MkfileOp
return a
}

// Symlink creates a symlink at `newpath` that points to `oldpath`
func (fa *FileAction) Symlink(oldpath, newpath string, opt ...SymlinkOption) *FileAction {
a := Symlink(oldpath, newpath, opt...)
a.prev = fa
return a
}

func (fa *FileAction) Rm(p string, opt ...RmOption) *FileAction {
a := Rm(p, opt...)
a.prev = fa
Expand Down Expand Up @@ -193,6 +200,7 @@ type ChownOption interface {
MkdirOption
MkfileOption
CopyOption
SymlinkOption
}

type mkdirOptionFunc func(*MkdirInfo)
Expand Down Expand Up @@ -290,6 +298,10 @@ func (co ChownOpt) SetCopyOption(mi *CopyInfo) {
mi.ChownOpt = &co
}

func (co ChownOpt) SetSymlinkOption(si *SymlinkInfo) {
si.ChownOpt = &co
}

func (co *ChownOpt) marshal(base pb.InputIndex) *pb.ChownOpt {
if co == nil {
return nil
Expand Down Expand Up @@ -337,6 +349,57 @@ func Mkfile(p string, m os.FileMode, dt []byte, opts ...MkfileOption) *FileActio
}
}

// SymlinkInfo is the modifiable options used to create symlinks
type SymlinkInfo struct {
ChownOpt *ChownOpt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mode as well

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the man pages, symlink permissions are irrelevant. Is the mode for the SUID, SGID, and sticky bits? What effect do those have on symlinks?

CreatedTime *time.Time
}

func (si *SymlinkInfo) SetSymlinkOption(si2 *SymlinkInfo) {
*si2 = *si
}

type SymlinkOption interface {
SetSymlinkOption(*SymlinkInfo)
}

// Symlink creates a symlink at `newpath` that points to `oldpath`
func Symlink(oldpath, newpath string, opts ...SymlinkOption) *FileAction {
var si SymlinkInfo
for _, o := range opts {
o.SetSymlinkOption(&si)
}

return &FileAction{
action: &fileActionSymlink{
oldpath: oldpath,
newpath: newpath,
info: si,
},
}
}

type fileActionSymlink struct {
oldpath string
newpath string
info SymlinkInfo
}

func (s *fileActionSymlink) addCaps(f *FileOp) {
addCap(&f.constraints, pb.CapFileSymlinkCreate)
}

func (s *fileActionSymlink) toProtoAction(_ context.Context, _ string, base pb.InputIndex) (pb.IsFileAction, error) {
return &pb.FileAction_Symlink{
Symlink: &pb.FileActionSymlink{
Oldpath: s.oldpath,
Newpath: s.newpath,
Owner: s.info.ChownOpt.marshal(base),
Timestamp: marshalTime(s.info.CreatedTime),
},
}, nil
}

type MkfileOption interface {
SetMkfileOption(*MkfileInfo)
}
Expand Down Expand Up @@ -606,6 +669,10 @@ func (c CreatedTime) SetMkfileOption(mi *MkfileInfo) {
mi.CreatedTime = (*time.Time)(&c)
}

func (c CreatedTime) SetSymlinkOption(si *SymlinkInfo) {
si.CreatedTime = (*time.Time)(&c)
}

func (c CreatedTime) SetCopyOption(mi *CopyInfo) {
mi.CreatedTime = (*time.Time)(&c)
}
Expand Down
35 changes: 35 additions & 0 deletions client/llb/fileop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,41 @@ func TestFileMkfile(t *testing.T) {
require.Equal(t, int64(-1), mkdir.Timestamp)
}

func TestFileSymlink(t *testing.T) {
t.Parallel()

st := Scratch().
File(Mkfile("/foo", 0700, []byte("data"))).
File(Symlink("foo", "/bar"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have some tests that chain symlink together with other actions. Atm it is unclear what the point of Mkfile is in this test as it is in a different FileOp that is never checked after marshaling.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the result of copying and pasting from another test. To be clear about your intent, you want to be sure that llb.Symlink plays nicely with other file actions when chained together, i.e. they happen in the right order?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The actions are all linked together so that the input points to either the input of the fileop or the offset of another action that needs to run before it

def, err := st.Marshal(context.TODO())

require.NoError(t, err)

m, arr := parseDef(t, def.Def)
require.Equal(t, 3, len(arr))

dgst, idx := last(t, arr)
require.Equal(t, 0, idx)
require.Equal(t, m[dgst], arr[1])

f := arr[1].Op.(*pb.Op_File).File
require.Equal(t, 1, len(arr[1].Inputs))
require.Equal(t, m[arr[1].Inputs[0].Digest], arr[0])
require.Equal(t, 0, int(arr[1].Inputs[0].Index))

require.Equal(t, 1, len(f.Actions))

action := f.Actions[0]
require.Equal(t, 0, int(action.Input))
require.Equal(t, -1, int(action.SecondaryInput))
require.Equal(t, 0, int(action.Output))

symlink := action.Action.(*pb.FileAction_Symlink).Symlink

require.Equal(t, "foo", symlink.Oldpath)
require.Equal(t, "/bar", symlink.Newpath)
}

func TestFileRm(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 2 additions & 0 deletions cmd/buildctl/debug/dumpllb.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ func attr(dgst digest.Digest, op *pb.Op) (string, string) {
name = fmt.Sprintf("mkdir{path=%s}", act.Mkdir.Path)
case *pb.FileAction_Rm:
name = fmt.Sprintf("rm{path=%s}", act.Rm.Path)
case *pb.FileAction_Symlink:
name = fmt.Sprintf("symlink{oldpath=%s, newpath=%s}", act.Symlink.Oldpath, act.Symlink.Newpath)
}

names = append(names, name)
Expand Down
55 changes: 55 additions & 0 deletions solver/llbsolver/file/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,40 @@ func mkdir(d string, action *pb.FileActionMkDir, user *copy.User, idmap *idtools
return nil
}

func symlink(d string, action *pb.FileActionSymlink, user *copy.User, idmap *idtools.IdentityMapping) (err error) {
defer func() {
var osErr *os.PathError
if errors.As(err, &osErr) {
// remove system root from error path if present
osErr.Path = strings.TrimPrefix(osErr.Path, d)
}
}()

newpath, err := fs.RootPath(d, filepath.Join("/", action.Newpath))
if err != nil {
return errors.WithStack(err)
}

ch, err := mapUserToChowner(user, idmap)
if err != nil {
return err
}

if err := os.Symlink(action.Oldpath, newpath); err != nil {
return errors.WithStack(err)
}

if err := copy.Chown(newpath, nil, ch); err != nil {
return errors.WithStack(err)
}

if err := copy.Utimes(newpath, timestampToTime(action.Timestamp)); err != nil {
return errors.WithStack(err)
}

return nil
}

func mkfile(d string, action *pb.FileActionMkFile, user *copy.User, idmap *idtools.IdentityMapping) (err error) {
defer func() {
var osErr *os.PathError
Expand Down Expand Up @@ -304,6 +338,27 @@ func (fb *Backend) Mkfile(ctx context.Context, m, user, group fileoptypes.Mount,
return mkfile(dir, action, u, mnt.m.IdentityMapping())
}

func (fb *Backend) Symlink(ctx context.Context, m, user, group fileoptypes.Mount, action *pb.FileActionSymlink) error {
mnt, ok := m.(*Mount)
if !ok {
return errors.Errorf("invalid mount type %T", m)
}

lm := snapshot.LocalMounter(mnt.m)
dir, err := lm.Mount()
if err != nil {
return err
}
defer lm.Unmount()

u, err := fb.readUserWrapper(action.Owner, user, group)
if err != nil {
return err
}

return symlink(dir, action, u, mnt.m.IdentityMapping())
}

func (fb *Backend) Rm(ctx context.Context, m fileoptypes.Mount, action *pb.FileActionRm) error {
mnt, ok := m.(*Mount)
if !ok {
Expand Down
15 changes: 15 additions & 0 deletions solver/llbsolver/ops/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ func (f *fileOp) CacheMap(ctx context.Context, g session.Group, index int) (*sol
if err != nil {
return nil, false, err
}
case *pb.FileAction_Symlink:
p := a.Symlink.CloneVT()
markInvalid(action.Input)
dt, err = json.Marshal(p)
if err != nil {
return nil, false, err
}
case *pb.FileAction_Rm:
p := a.Rm.CloneVT()
markInvalid(action.Input)
Expand Down Expand Up @@ -586,6 +593,14 @@ func (s *FileOpSolver) getInput(ctx context.Context, idx int, inputs []fileoptyp
if err := s.b.Mkdir(ctx, inpMount, user, group, a.Mkdir); err != nil {
return input{}, err
}
case *pb.FileAction_Symlink:
user, group, err := loadOwner(ctx, a.Symlink.Owner)
if err != nil {
return input{}, err
}
if err := s.b.Symlink(ctx, inpMount, user, group, a.Symlink); err != nil {
return input{}, err
}
case *pb.FileAction_Mkfile:
user, group, err := loadOwner(ctx, a.Mkfile.Owner)
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions solver/llbsolver/ops/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ type mod struct {
rm *pb.FileActionRm
mkfile *pb.FileActionMkFile
copy *pb.FileActionCopy
symlink *pb.FileActionSymlink
copySrc []mod
}

Expand Down Expand Up @@ -643,6 +644,13 @@ func (b *testFileBackend) Mkfile(_ context.Context, m, user, group fileoptypes.M
return nil
}

func (b *testFileBackend) Symlink(_ context.Context, m, user, group fileoptypes.Mount, a *pb.FileActionSymlink) error {
mm := m.(*testMount)
mm.id += "-symlink"
mm.chain = append(mm.chain, mod{symlink: a})
return nil
}

func (b *testFileBackend) Rm(_ context.Context, m fileoptypes.Mount, a *pb.FileActionRm) error {
mm := m.(*testMount)
mm.id += "-rm"
Expand Down
1 change: 1 addition & 0 deletions solver/llbsolver/ops/fileoptypes/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Mount interface {

type Backend interface {
Mkdir(context.Context, Mount, Mount, Mount, *pb.FileActionMkDir) error
Symlink(context.Context, Mount, Mount, Mount, *pb.FileActionSymlink) error
Mkfile(context.Context, Mount, Mount, Mount, *pb.FileActionMkFile) error
Rm(context.Context, Mount, *pb.FileActionRm) error
Copy(context.Context, Mount, Mount, Mount, Mount, *pb.FileActionCopy) error
Expand Down
2 changes: 2 additions & 0 deletions solver/llbsolver/vertex.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@ func fileOpName(actions []*pb.FileAction) string {
names = append(names, fmt.Sprintf("mkdir %s", a.Mkdir.Path))
case *pb.FileAction_Mkfile:
names = append(names, fmt.Sprintf("mkfile %s", a.Mkfile.Path))
case *pb.FileAction_Symlink:
names = append(names, fmt.Sprintf("symlink %s -> %s", a.Symlink.Newpath, a.Symlink.Oldpath))
case *pb.FileAction_Rm:
names = append(names, fmt.Sprintf("rm %s", a.Rm.Path))
case *pb.FileAction_Copy:
Expand Down
Loading