Skip to content

Commit 147bf0e

Browse files
authored
Merge pull request moby#5519 from pmengelbert/pmengelbert/add_llb_symlinks/1
Add FileActionSymlink and `llb.Symlink`
2 parents 0303304 + 20c2d03 commit 147bf0e

File tree

13 files changed

+1036
-194
lines changed

13 files changed

+1036
-194
lines changed

client/client_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
222222
testLayerLimitOnMounts,
223223
testFrontendVerifyPlatforms,
224224
testRunValidExitCodes,
225+
testFileOpSymlink,
225226
}
226227

227228
func TestIntegration(t *testing.T) {
@@ -2426,6 +2427,95 @@ func testOCILayoutPlatformSource(t *testing.T, sb integration.Sandbox) {
24262427
}
24272428
}
24282429

2430+
func testFileOpSymlink(t *testing.T, sb integration.Sandbox) {
2431+
requiresLinux(t)
2432+
2433+
const (
2434+
fileOwner = 7777
2435+
fileGroup = 8888
2436+
linkOwner = 1111
2437+
linkGroup = 2222
2438+
2439+
dummyTimestamp = 42
2440+
)
2441+
2442+
dummyTime := time.Unix(dummyTimestamp, 0)
2443+
2444+
c, err := New(sb.Context(), sb.Address())
2445+
require.NoError(t, err)
2446+
defer c.Close()
2447+
2448+
st := llb.Scratch().
2449+
File(llb.Mkdir("/foo", 0700).Mkfile("bar", 0600, []byte("contents"), llb.ChownOpt{
2450+
User: &llb.UserOpt{
2451+
UID: fileOwner,
2452+
},
2453+
Group: &llb.UserOpt{
2454+
UID: fileGroup,
2455+
},
2456+
})).
2457+
File(llb.Symlink("bar", "/baz", llb.WithCreatedTime(dummyTime), llb.ChownOpt{
2458+
User: &llb.UserOpt{
2459+
UID: linkOwner,
2460+
},
2461+
Group: &llb.UserOpt{
2462+
UID: linkGroup,
2463+
},
2464+
}))
2465+
2466+
def, err := st.Marshal(sb.Context())
2467+
require.NoError(t, err)
2468+
2469+
destDir := t.TempDir()
2470+
2471+
out := filepath.Join(destDir, "out.tar")
2472+
outW, err := os.Create(out)
2473+
require.NoError(t, err)
2474+
defer outW.Close()
2475+
2476+
_, err = c.Solve(sb.Context(), def, SolveOpt{
2477+
Exports: []ExportEntry{
2478+
{
2479+
Type: ExporterTar,
2480+
Output: fixedWriteCloser(outW),
2481+
},
2482+
},
2483+
}, nil)
2484+
require.NoError(t, err)
2485+
2486+
dt, err := os.ReadFile(out)
2487+
require.NoError(t, err)
2488+
m, err := testutil.ReadTarToMap(dt, false)
2489+
require.NoError(t, err)
2490+
2491+
entry, ok := m["bar"]
2492+
require.True(t, ok)
2493+
2494+
dt = entry.Data
2495+
header := entry.Header
2496+
require.NoError(t, err)
2497+
2498+
require.Equal(t, []byte("contents"), dt)
2499+
require.Equal(t, fileOwner, header.Uid)
2500+
require.Equal(t, fileGroup, header.Gid)
2501+
2502+
entry, ok = m["baz"]
2503+
require.Equal(t, true, ok)
2504+
2505+
header = entry.Header
2506+
// ensure it is a symlink
2507+
require.Equal(t, tar.TypeSymlink, rune(header.Typeflag))
2508+
// ensure it is a symlink to the proper location
2509+
require.Equal(t, "bar", header.Linkname)
2510+
2511+
// make sure it was chowned properly
2512+
require.Equal(t, linkOwner, header.Uid)
2513+
require.Equal(t, linkGroup, header.Gid)
2514+
2515+
// ensure it was timestamped properly
2516+
require.Equal(t, dummyTime, header.ModTime)
2517+
}
2518+
24292519
func testFileOpRmWildcard(t *testing.T, sb integration.Sandbox) {
24302520
requiresLinux(t)
24312521
c, err := New(sb.Context(), sb.Address())

client/llb/fileop.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ func (fa *FileAction) Mkfile(p string, m os.FileMode, dt []byte, opt ...MkfileOp
8585
return a
8686
}
8787

88+
// Symlink creates a symlink at `newpath` that points to `oldpath`
89+
func (fa *FileAction) Symlink(oldpath, newpath string, opt ...SymlinkOption) *FileAction {
90+
a := Symlink(oldpath, newpath, opt...)
91+
a.prev = fa
92+
return a
93+
}
94+
8895
func (fa *FileAction) Rm(p string, opt ...RmOption) *FileAction {
8996
a := Rm(p, opt...)
9097
a.prev = fa
@@ -193,6 +200,7 @@ type ChownOption interface {
193200
MkdirOption
194201
MkfileOption
195202
CopyOption
203+
SymlinkOption
196204
}
197205

198206
type mkdirOptionFunc func(*MkdirInfo)
@@ -290,6 +298,10 @@ func (co ChownOpt) SetCopyOption(mi *CopyInfo) {
290298
mi.ChownOpt = &co
291299
}
292300

301+
func (co ChownOpt) SetSymlinkOption(si *SymlinkInfo) {
302+
si.ChownOpt = &co
303+
}
304+
293305
func (co *ChownOpt) marshal(base pb.InputIndex) *pb.ChownOpt {
294306
if co == nil {
295307
return nil
@@ -337,6 +349,57 @@ func Mkfile(p string, m os.FileMode, dt []byte, opts ...MkfileOption) *FileActio
337349
}
338350
}
339351

352+
// SymlinkInfo is the modifiable options used to create symlinks
353+
type SymlinkInfo struct {
354+
ChownOpt *ChownOpt
355+
CreatedTime *time.Time
356+
}
357+
358+
func (si *SymlinkInfo) SetSymlinkOption(si2 *SymlinkInfo) {
359+
*si2 = *si
360+
}
361+
362+
type SymlinkOption interface {
363+
SetSymlinkOption(*SymlinkInfo)
364+
}
365+
366+
// Symlink creates a symlink at `newpath` that points to `oldpath`
367+
func Symlink(oldpath, newpath string, opts ...SymlinkOption) *FileAction {
368+
var si SymlinkInfo
369+
for _, o := range opts {
370+
o.SetSymlinkOption(&si)
371+
}
372+
373+
return &FileAction{
374+
action: &fileActionSymlink{
375+
oldpath: oldpath,
376+
newpath: newpath,
377+
info: si,
378+
},
379+
}
380+
}
381+
382+
type fileActionSymlink struct {
383+
oldpath string
384+
newpath string
385+
info SymlinkInfo
386+
}
387+
388+
func (s *fileActionSymlink) addCaps(f *FileOp) {
389+
addCap(&f.constraints, pb.CapFileSymlinkCreate)
390+
}
391+
392+
func (s *fileActionSymlink) toProtoAction(_ context.Context, _ string, base pb.InputIndex) (pb.IsFileAction, error) {
393+
return &pb.FileAction_Symlink{
394+
Symlink: &pb.FileActionSymlink{
395+
Oldpath: s.oldpath,
396+
Newpath: s.newpath,
397+
Owner: s.info.ChownOpt.marshal(base),
398+
Timestamp: marshalTime(s.info.CreatedTime),
399+
},
400+
}, nil
401+
}
402+
340403
type MkfileOption interface {
341404
SetMkfileOption(*MkfileInfo)
342405
}
@@ -606,6 +669,10 @@ func (c CreatedTime) SetMkfileOption(mi *MkfileInfo) {
606669
mi.CreatedTime = (*time.Time)(&c)
607670
}
608671

672+
func (c CreatedTime) SetSymlinkOption(si *SymlinkInfo) {
673+
si.CreatedTime = (*time.Time)(&c)
674+
}
675+
609676
func (c CreatedTime) SetCopyOption(mi *CopyInfo) {
610677
mi.CreatedTime = (*time.Time)(&c)
611678
}

client/llb/fileop_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,67 @@ func TestFileMkfile(t *testing.T) {
179179
require.Equal(t, int64(-1), mkdir.Timestamp)
180180
}
181181

182+
func TestFileSymlink(t *testing.T) {
183+
t.Parallel()
184+
185+
st := Image("foo").Dir("/src").File(
186+
Mkdir("dir", 0o755).
187+
Symlink("/src/dir", "/srcdir").
188+
Mkfile("/srcdir/file", 0700, []byte("asdfjkl;")).
189+
Symlink("dir/file", "/srcdirfile").
190+
Mkdir("/src/dir/subdir", 0o755).
191+
Symlink("/src/dir/subdir", "/src/dir/subdir/nested"))
192+
193+
const numOps = 2
194+
const numActions = 6
195+
def, err := st.Marshal(context.TODO())
196+
197+
require.NoError(t, err)
198+
199+
m, arr := parseDef(t, def.Def)
200+
require.Equal(t, numOps+1, len(arr))
201+
202+
dgst, idx := last(t, arr)
203+
require.Equal(t, 0, idx)
204+
require.Equal(t, m[dgst], arr[numOps-1])
205+
206+
fileOpNode := arr[1]
207+
fileOp := fileOpNode.Op.(*pb.Op_File).File
208+
require.Equal(t, numActions, len(fileOp.Actions))
209+
require.Equal(t, 1, len(fileOpNode.Inputs))
210+
require.Equal(t, m[fileOpNode.Inputs[0].Digest], arr[0])
211+
require.Equal(t, 0, int(fileOpNode.Inputs[0].Index))
212+
213+
symlinkTests := []*pb.FileActionSymlink{
214+
nil,
215+
{Oldpath: "/src/dir", Newpath: "/srcdir"},
216+
nil,
217+
{Oldpath: "dir/file", Newpath: "/srcdirfile"},
218+
nil,
219+
{Oldpath: "/src/dir/subdir", Newpath: "/src/dir/subdir/nested"},
220+
}
221+
222+
for i := 0; i < numActions; i++ {
223+
expectedOutput := -1
224+
if i == numActions-1 {
225+
expectedOutput = 0
226+
}
227+
228+
require.Equal(t, int(fileOp.Actions[i].Input), i)
229+
require.Equal(t, -1, int(fileOp.Actions[i].SecondaryInput))
230+
require.Equal(t, expectedOutput, int(fileOp.Actions[i].Output))
231+
232+
if symlinkTests[i] == nil {
233+
continue
234+
}
235+
236+
symlink := fileOp.Actions[i].Action.(*pb.FileAction_Symlink).Symlink
237+
238+
require.Equal(t, symlink.Oldpath, symlinkTests[i].Oldpath)
239+
require.Equal(t, symlink.Newpath, symlinkTests[i].Newpath)
240+
}
241+
}
242+
182243
func TestFileRm(t *testing.T) {
183244
t.Parallel()
184245

cmd/buildctl/debug/dumpllb.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ func attr(dgst digest.Digest, op *pb.Op) (string, string) {
130130
name = fmt.Sprintf("mkdir{path=%s}", act.Mkdir.Path)
131131
case *pb.FileAction_Rm:
132132
name = fmt.Sprintf("rm{path=%s}", act.Rm.Path)
133+
case *pb.FileAction_Symlink:
134+
name = fmt.Sprintf("symlink{oldpath=%s, newpath=%s}", act.Symlink.Oldpath, act.Symlink.Newpath)
133135
}
134136

135137
names = append(names, name)

solver/llbsolver/file/backend.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,40 @@ func mkdir(d string, action *pb.FileActionMkDir, user *copy.User, idmap *idtools
6767
return nil
6868
}
6969

70+
func symlink(d string, action *pb.FileActionSymlink, user *copy.User, idmap *idtools.IdentityMapping) (err error) {
71+
defer func() {
72+
var osErr *os.PathError
73+
if errors.As(err, &osErr) {
74+
// remove system root from error path if present
75+
osErr.Path = strings.TrimPrefix(osErr.Path, d)
76+
}
77+
}()
78+
79+
newpath, err := fs.RootPath(d, filepath.Join("/", action.Newpath))
80+
if err != nil {
81+
return errors.WithStack(err)
82+
}
83+
84+
ch, err := mapUserToChowner(user, idmap)
85+
if err != nil {
86+
return err
87+
}
88+
89+
if err := os.Symlink(action.Oldpath, newpath); err != nil {
90+
return errors.WithStack(err)
91+
}
92+
93+
if err := copy.Chown(newpath, nil, ch); err != nil {
94+
return errors.WithStack(err)
95+
}
96+
97+
if err := copy.Utimes(newpath, timestampToTime(action.Timestamp)); err != nil {
98+
return errors.WithStack(err)
99+
}
100+
101+
return nil
102+
}
103+
70104
func mkfile(d string, action *pb.FileActionMkFile, user *copy.User, idmap *idtools.IdentityMapping) (err error) {
71105
defer func() {
72106
var osErr *os.PathError
@@ -304,6 +338,27 @@ func (fb *Backend) Mkfile(ctx context.Context, m, user, group fileoptypes.Mount,
304338
return mkfile(dir, action, u, mnt.m.IdentityMapping())
305339
}
306340

341+
func (fb *Backend) Symlink(ctx context.Context, m, user, group fileoptypes.Mount, action *pb.FileActionSymlink) error {
342+
mnt, ok := m.(*Mount)
343+
if !ok {
344+
return errors.Errorf("invalid mount type %T", m)
345+
}
346+
347+
lm := snapshot.LocalMounter(mnt.m)
348+
dir, err := lm.Mount()
349+
if err != nil {
350+
return err
351+
}
352+
defer lm.Unmount()
353+
354+
u, err := fb.readUserWrapper(action.Owner, user, group)
355+
if err != nil {
356+
return err
357+
}
358+
359+
return symlink(dir, action, u, mnt.m.IdentityMapping())
360+
}
361+
307362
func (fb *Backend) Rm(ctx context.Context, m fileoptypes.Mount, action *pb.FileActionRm) error {
308363
mnt, ok := m.(*Mount)
309364
if !ok {

solver/llbsolver/ops/file.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ func (f *fileOp) CacheMap(ctx context.Context, g session.Group, index int) (*sol
8484
if err != nil {
8585
return nil, false, err
8686
}
87+
case *pb.FileAction_Symlink:
88+
p := a.Symlink.CloneVT()
89+
markInvalid(action.Input)
90+
dt, err = json.Marshal(p)
91+
if err != nil {
92+
return nil, false, err
93+
}
8794
case *pb.FileAction_Rm:
8895
p := a.Rm.CloneVT()
8996
markInvalid(action.Input)
@@ -586,6 +593,14 @@ func (s *FileOpSolver) getInput(ctx context.Context, idx int, inputs []fileoptyp
586593
if err := s.b.Mkdir(ctx, inpMount, user, group, a.Mkdir); err != nil {
587594
return input{}, err
588595
}
596+
case *pb.FileAction_Symlink:
597+
user, group, err := loadOwner(ctx, a.Symlink.Owner)
598+
if err != nil {
599+
return input{}, err
600+
}
601+
if err := s.b.Symlink(ctx, inpMount, user, group, a.Symlink); err != nil {
602+
return input{}, err
603+
}
589604
case *pb.FileAction_Mkfile:
590605
user, group, err := loadOwner(ctx, a.Mkfile.Owner)
591606
if err != nil {

0 commit comments

Comments
 (0)