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

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
90 changes: 90 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testLayerLimitOnMounts,
testFrontendVerifyPlatforms,
testRunValidExitCodes,
testFileOpSymlink,
}

func TestIntegration(t *testing.T) {
Expand Down Expand Up @@ -2426,6 +2427,95 @@ 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"]
require.Equal(t, true, ok)

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
Contributor 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
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Member

Choose a reason for hiding this comment

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

Add header.Typeflag symlink check as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

// ensure it is a symlink
require.Equal(t, tar.TypeSymlink, rune(header.Typeflag))
// ensure it is a symlink to the proper location
require.Equal(t, "bar", header.Linkname)

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

// ensure it was timestamped properly
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
Contributor 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
61 changes: 61 additions & 0 deletions client/llb/fileop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,67 @@ func TestFileMkfile(t *testing.T) {
require.Equal(t, int64(-1), mkdir.Timestamp)
}

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

st := Image("foo").Dir("/src").File(
Mkdir("dir", 0o755).
Symlink("/src/dir", "/srcdir").
Mkfile("/srcdir/file", 0700, []byte("asdfjkl;")).
Symlink("dir/file", "/srcdirfile").
Mkdir("/src/dir/subdir", 0o755).
Symlink("/src/dir/subdir", "/src/dir/subdir/nested"))

const numOps = 2
const numActions = 6
def, err := st.Marshal(context.TODO())

require.NoError(t, err)

m, arr := parseDef(t, def.Def)
require.Equal(t, numOps+1, len(arr))

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

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

symlinkTests := []*pb.FileActionSymlink{
nil,
{Oldpath: "/src/dir", Newpath: "/srcdir"},
nil,
{Oldpath: "dir/file", Newpath: "/srcdirfile"},
nil,
{Oldpath: "/src/dir/subdir", Newpath: "/src/dir/subdir/nested"},
}

for i := 0; i < numActions; i++ {
expectedOutput := -1
if i == numActions-1 {
expectedOutput = 0
}

require.Equal(t, int(fileOp.Actions[i].Input), i)
require.Equal(t, -1, int(fileOp.Actions[i].SecondaryInput))
require.Equal(t, expectedOutput, int(fileOp.Actions[i].Output))

if symlinkTests[i] == nil {
continue
}

symlink := fileOp.Actions[i].Action.(*pb.FileAction_Symlink).Symlink

require.Equal(t, symlink.Oldpath, symlinkTests[i].Oldpath)
require.Equal(t, symlink.Newpath, symlinkTests[i].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
Loading
Loading