Skip to content

Commit 64096ea

Browse files
committed
gateway: create interface for reading from container filesystem
This creates an interface that can be used to read the filesystem of a new container created through the gateway API. These filesystem reading methods are tied to a specific container that has been created, but aren't tied to the container itself. Due to being run inside of buildkit, these containers have access to the same mounts that a container request would have. This is useful for features like the file explorer in `buildx dap` because it can access container filesystem state from stages that error along with ones that have completed successfully. Signed-off-by: Jonathan A. Sternberg <[email protected]>
1 parent c132bdf commit 64096ea

File tree

10 files changed

+535
-35
lines changed

10 files changed

+535
-35
lines changed

client/build.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,36 @@ func (g *gatewayClientForBuild) ReleaseContainer(ctx context.Context, in *gatewa
178178
return g.gateway.ReleaseContainer(ctx, in, opts...)
179179
}
180180

181+
func (g *gatewayClientForBuild) ReadFileContainer(ctx context.Context, in *gatewayapi.ReadFileRequest, opts ...grpc.CallOption) (*gatewayapi.ReadFileResponse, error) {
182+
if g.caps != nil {
183+
if err := g.caps.Supports(gatewayapi.CapGatewayExecFilesystem); err != nil {
184+
return nil, err
185+
}
186+
}
187+
ctx = buildid.AppendToOutgoingContext(ctx, g.buildID)
188+
return g.gateway.ReadFileContainer(ctx, in, opts...)
189+
}
190+
191+
func (g *gatewayClientForBuild) ReadDirContainer(ctx context.Context, in *gatewayapi.ReadDirRequest, opts ...grpc.CallOption) (*gatewayapi.ReadDirResponse, error) {
192+
if g.caps != nil {
193+
if err := g.caps.Supports(gatewayapi.CapGatewayExecFilesystem); err != nil {
194+
return nil, err
195+
}
196+
}
197+
ctx = buildid.AppendToOutgoingContext(ctx, g.buildID)
198+
return g.gateway.ReadDirContainer(ctx, in, opts...)
199+
}
200+
201+
func (g *gatewayClientForBuild) StatFileContainer(ctx context.Context, in *gatewayapi.StatFileRequest, opts ...grpc.CallOption) (*gatewayapi.StatFileResponse, error) {
202+
if g.caps != nil {
203+
if err := g.caps.Supports(gatewayapi.CapGatewayExecFilesystem); err != nil {
204+
return nil, err
205+
}
206+
}
207+
ctx = buildid.AppendToOutgoingContext(ctx, g.buildID)
208+
return g.gateway.StatFileContainer(ctx, in, opts...)
209+
}
210+
181211
func (g *gatewayClientForBuild) ExecProcess(ctx context.Context, opts ...grpc.CallOption) (gatewayapi.LLBBridge_ExecProcessClient, error) {
182212
if g.caps != nil {
183213
if err := g.caps.Supports(gatewayapi.CapGatewayExec); err != nil {

control/gateway/gateway.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,30 @@ func (gwf *GatewayForwarder) ReleaseContainer(ctx context.Context, req *gwapi.Re
188188
return fwd.ReleaseContainer(ctx, req)
189189
}
190190

191+
func (gwf *GatewayForwarder) ReadFileContainer(ctx context.Context, req *gwapi.ReadFileRequest) (*gwapi.ReadFileResponse, error) {
192+
fwd, err := gwf.lookupForwarder(ctx)
193+
if err != nil {
194+
return nil, errors.Wrap(err, "forwarding ReadFileContainer")
195+
}
196+
return fwd.ReadFileContainer(ctx, req)
197+
}
198+
199+
func (gwf *GatewayForwarder) ReadDirContainer(ctx context.Context, req *gwapi.ReadDirRequest) (*gwapi.ReadDirResponse, error) {
200+
fwd, err := gwf.lookupForwarder(ctx)
201+
if err != nil {
202+
return nil, errors.Wrap(err, "forwarding ReadDirContainer")
203+
}
204+
return fwd.ReadDirContainer(ctx, req)
205+
}
206+
207+
func (gwf *GatewayForwarder) StatFileContainer(ctx context.Context, req *gwapi.StatFileRequest) (*gwapi.StatFileResponse, error) {
208+
fwd, err := gwf.lookupForwarder(ctx)
209+
if err != nil {
210+
return nil, errors.Wrap(err, "forwarding StatFileContainer")
211+
}
212+
return fwd.StatFileContainer(ctx, req)
213+
}
214+
191215
func (gwf *GatewayForwarder) ExecProcess(srv gwapi.LLBBridge_ExecProcessServer) error {
192216
fwd, err := gwf.lookupForwarder(srv.Context())
193217
if err != nil {

frontend/gateway/client/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type Mount struct {
6666
type Container interface {
6767
Start(context.Context, StartRequest) (ContainerProcess, error)
6868
Release(context.Context) error
69+
MountReference
6970
}
7071

7172
// StartRequest encapsulates the arguments to define a process within a
@@ -101,6 +102,10 @@ type ContainerProcess interface {
101102
type Reference interface {
102103
ToState() (llb.State, error)
103104
Evaluate(ctx context.Context) error
105+
MountReference
106+
}
107+
108+
type MountReference interface {
104109
ReadFile(ctx context.Context, req ReadRequest) ([]byte, error)
105110
StatFile(ctx context.Context, req StatRequest) (*fstypes.Stat, error)
106111
ReadDir(ctx context.Context, req ReadDirRequest) ([]*fstypes.Stat, error)

frontend/gateway/container/container.go

Lines changed: 179 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"cmp"
55
"context"
66
"fmt"
7+
"io/fs"
8+
"os"
79
"path/filepath"
810
"runtime"
911
"slices"
@@ -25,6 +27,7 @@ import (
2527
"github.com/moby/buildkit/util/stack"
2628
"github.com/moby/buildkit/worker"
2729
"github.com/pkg/errors"
30+
fstypes "github.com/tonistiigi/fsutil/types"
2831
"golang.org/x/sync/errgroup"
2932
)
3033

@@ -281,22 +284,23 @@ func PrepareMounts(ctx context.Context, mm *mounts.MountManager, cm cache.Manage
281284
}
282285

283286
type gatewayContainer struct {
284-
id string
285-
netMode opspb.NetMode
286-
hostname string
287-
extraHosts []executor.HostIP
288-
platform *opspb.Platform
289-
rootFS executor.Mount
290-
mounts []executor.Mount
291-
executor executor.Executor
292-
sm *session.Manager
293-
group session.Group
294-
started bool
295-
errGroup *errgroup.Group
296-
mu sync.Mutex
297-
cleanup []func() error
298-
ctx context.Context
299-
cancel func(error)
287+
id string
288+
netMode opspb.NetMode
289+
hostname string
290+
extraHosts []executor.HostIP
291+
platform *opspb.Platform
292+
rootFS executor.Mount
293+
mounts []executor.Mount
294+
executor executor.Executor
295+
sm *session.Manager
296+
group session.Group
297+
started bool
298+
errGroup *errgroup.Group
299+
mu sync.Mutex
300+
cleanup []func() error
301+
ctx context.Context
302+
cancel func(error)
303+
localMounts map[executor.Mount]fs.FS
300304
}
301305

302306
func (gwCtr *gatewayContainer) Start(ctx context.Context, req client.StartRequest) (client.ContainerProcess, error) {
@@ -419,6 +423,124 @@ func (gwCtr *gatewayContainer) Release(ctx context.Context) error {
419423
return stack.Enable(err2)
420424
}
421425

426+
func (gwCtr *gatewayContainer) ReadFile(ctx context.Context, req client.ReadRequest) ([]byte, error) {
427+
fsys, path, err := gwCtr.mount(ctx, req.Filename)
428+
if err != nil {
429+
return nil, err
430+
}
431+
return fs.ReadFile(fsys, path)
432+
}
433+
434+
func (gwCtr *gatewayContainer) ReadDir(ctx context.Context, req client.ReadDirRequest) ([]*fstypes.Stat, error) {
435+
fsys, path, err := gwCtr.mount(ctx, req.Path)
436+
if err != nil {
437+
return nil, err
438+
}
439+
440+
entries, err := fs.ReadDir(fsys, path)
441+
if err != nil {
442+
return nil, err
443+
}
444+
445+
files := make([]*fstypes.Stat, len(entries))
446+
for i, e := range entries {
447+
fullpath := filepath.Join(path, e.Name())
448+
fi, err := e.Info()
449+
if err != nil {
450+
return nil, err
451+
}
452+
453+
files[i], err = mkstat(fsys, fullpath, e.Name(), fi)
454+
if err != nil {
455+
return nil, errors.Wrap(err, "mkstat")
456+
}
457+
}
458+
return files, nil
459+
}
460+
461+
func (gwCtr *gatewayContainer) StatFile(ctx context.Context, req client.StatRequest) (*fstypes.Stat, error) {
462+
fsys, path, err := gwCtr.mount(ctx, req.Path)
463+
if err != nil {
464+
return nil, err
465+
}
466+
467+
fi, err := fs.Stat(fsys, path)
468+
if err != nil {
469+
return nil, err
470+
}
471+
return mkstat(fsys, req.Path, filepath.Base(req.Path), fi)
472+
}
473+
474+
func (gwCtr *gatewayContainer) mount(ctx context.Context, fullpath string) (fs.FS, string, error) {
475+
mount, path := gwCtr.findMount(ctx, fullpath)
476+
477+
gwCtr.mu.Lock()
478+
defer gwCtr.mu.Unlock()
479+
480+
// Check if this mount has already been mounted.
481+
if f, ok := gwCtr.localMounts[mount]; ok {
482+
return f, path, nil
483+
}
484+
485+
ref, err := mount.Src.Mount(ctx, true)
486+
if err != nil {
487+
return nil, "", err
488+
}
489+
490+
mounter := snapshot.LocalMounter(ref)
491+
dir, err := mounter.Mount()
492+
if err != nil {
493+
return nil, "", err
494+
}
495+
496+
// Register cleanup.
497+
gwCtr.cleanup = append(gwCtr.cleanup, func() error {
498+
return mounter.Unmount()
499+
})
500+
501+
root, err := os.OpenRoot(dir)
502+
if err != nil {
503+
return nil, "", err
504+
}
505+
506+
gwCtr.cleanup = append(gwCtr.cleanup, func() error {
507+
return root.Close()
508+
})
509+
510+
if gwCtr.localMounts == nil {
511+
gwCtr.localMounts = make(map[executor.Mount]fs.FS)
512+
}
513+
514+
f := root.FS()
515+
gwCtr.localMounts[mount] = f
516+
return f, path, nil
517+
}
518+
519+
func (gwCtr *gatewayContainer) findMount(ctx context.Context, fullpath string) (m executor.Mount, path string) {
520+
m = gwCtr.rootFS
521+
path, _ = filepath.Rel("/", fullpath)
522+
if len(gwCtr.mounts) == 0 {
523+
return m, path
524+
}
525+
526+
for _, mount := range gwCtr.mounts {
527+
if strings.HasPrefix(fullpath, mount.Dest) {
528+
remainder, err := filepath.Rel(mount.Dest, fullpath)
529+
if err != nil {
530+
bklog.G(ctx).Warnf("skipping mount at %q because it could not be converted into a relative path from %q", mount.Dest, fullpath)
531+
continue
532+
}
533+
534+
if len(remainder) < len(path) {
535+
// Prefix matches and the remaining path is shorter so the prefix
536+
// must be longer. This match works better.
537+
m, path = mount, remainder
538+
}
539+
}
540+
}
541+
return m, path
542+
}
543+
422544
type gatewayContainerProcess struct {
423545
errGroup *errgroup.Group
424546
groupCtx context.Context
@@ -511,3 +633,44 @@ type mountable struct {
511633
func (m *mountable) Mount(ctx context.Context, readonly bool) (snapshot.Mountable, error) {
512634
return m.m.Mount(ctx, readonly, m.g)
513635
}
636+
637+
// constructs a Stat object. path is where the path can be found right
638+
// now, relpath is the desired path to be recorded in the stat (so
639+
// relative to whatever base dir is relevant). fi is the os.Stat
640+
// info. inodemap is used to calculate hardlinks over a series of
641+
// mkstat calls and maps inode to the canonical (aka "first") path for
642+
// a set of hardlinks to that inode.
643+
func mkstat(fsys fs.FS, path, relpath string, fi os.FileInfo) (*fstypes.Stat, error) {
644+
relpath = filepath.ToSlash(relpath)
645+
646+
stat := &fstypes.Stat{
647+
Path: filepath.FromSlash(relpath),
648+
Mode: uint32(fi.Mode()),
649+
ModTime: fi.ModTime().UnixNano(),
650+
}
651+
652+
if !fi.IsDir() {
653+
stat.Size = fi.Size()
654+
if fi.Mode()&os.ModeSymlink != 0 {
655+
link, err := fs.ReadLink(fsys, path)
656+
if err != nil {
657+
return nil, errors.WithStack(err)
658+
}
659+
stat.Linkname = link
660+
}
661+
}
662+
663+
if runtime.GOOS == "windows" {
664+
permPart := stat.Mode & uint32(os.ModePerm)
665+
noPermPart := stat.Mode &^ uint32(os.ModePerm)
666+
// Add the x bit: make everything +x from windows
667+
permPart |= 0111
668+
permPart &= 0755
669+
stat.Mode = noPermPart | permPart
670+
}
671+
672+
// Clear the socket bit since archive/tar.FileInfoHeader does not handle it
673+
stat.Mode &^= uint32(os.ModeSocket)
674+
675+
return stat, nil
676+
}

frontend/gateway/gateway.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,84 @@ func (lbf *llbBridgeForwarder) NewContainer(ctx context.Context, in *pb.NewConta
11811181
return &pb.NewContainerResponse{}, nil
11821182
}
11831183

1184+
func (lbf *llbBridgeForwarder) ReadFileContainer(ctx context.Context, in *pb.ReadFileRequest) (*pb.ReadFileResponse, error) {
1185+
bklog.G(ctx).Debugf("|<--- ReadFileContainer %s", in.Ref)
1186+
lbf.ctrsMu.Lock()
1187+
ctr, ok := lbf.ctrs[in.Ref]
1188+
lbf.ctrsMu.Unlock()
1189+
if !ok {
1190+
return nil, errors.Errorf("container details for %s not found", in.Ref)
1191+
}
1192+
1193+
var fileRange *gwclient.FileRange
1194+
if in.Range != nil {
1195+
fileRange = &gwclient.FileRange{
1196+
Length: int(in.Range.Length),
1197+
Offset: int(in.Range.Offset),
1198+
}
1199+
}
1200+
req := gwclient.ReadRequest{
1201+
Filename: in.FilePath,
1202+
Range: fileRange,
1203+
}
1204+
1205+
data, err := ctr.ReadFile(ctx, req)
1206+
if err != nil {
1207+
return nil, err
1208+
}
1209+
1210+
return &pb.ReadFileResponse{
1211+
Data: data,
1212+
}, nil
1213+
}
1214+
1215+
func (lbf *llbBridgeForwarder) ReadDirContainer(ctx context.Context, in *pb.ReadDirRequest) (*pb.ReadDirResponse, error) {
1216+
bklog.G(ctx).Debugf("|<--- ReadDirContainer %s", in.Ref)
1217+
lbf.ctrsMu.Lock()
1218+
ctr, ok := lbf.ctrs[in.Ref]
1219+
lbf.ctrsMu.Unlock()
1220+
if !ok {
1221+
return nil, errors.Errorf("container details for %s not found", in.Ref)
1222+
}
1223+
1224+
req := gwclient.ReadDirRequest{
1225+
Path: in.DirPath,
1226+
IncludePattern: in.IncludePattern,
1227+
}
1228+
1229+
files, err := ctr.ReadDir(ctx, req)
1230+
if err != nil {
1231+
return nil, err
1232+
}
1233+
1234+
return &pb.ReadDirResponse{
1235+
Entries: files,
1236+
}, nil
1237+
}
1238+
1239+
func (lbf *llbBridgeForwarder) StatFileContainer(ctx context.Context, in *pb.StatFileRequest) (*pb.StatFileResponse, error) {
1240+
bklog.G(ctx).Debugf("|<--- StatFileContainer %s", in.Ref)
1241+
lbf.ctrsMu.Lock()
1242+
ctr, ok := lbf.ctrs[in.Ref]
1243+
lbf.ctrsMu.Unlock()
1244+
if !ok {
1245+
return nil, errors.Errorf("container details for %s not found", in.Ref)
1246+
}
1247+
1248+
req := gwclient.StatRequest{
1249+
Path: in.Path,
1250+
}
1251+
1252+
stat, err := ctr.StatFile(ctx, req)
1253+
if err != nil {
1254+
return nil, err
1255+
}
1256+
1257+
return &pb.StatFileResponse{
1258+
Stat: stat,
1259+
}, nil
1260+
}
1261+
11841262
func (lbf *llbBridgeForwarder) ReleaseContainer(ctx context.Context, in *pb.ReleaseContainerRequest) (*pb.ReleaseContainerResponse, error) {
11851263
bklog.G(ctx).Debugf("|<--- ReleaseContainer %s", in.ContainerID)
11861264
lbf.ctrsMu.Lock()

0 commit comments

Comments
 (0)