Skip to content

Commit 177e3d8

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.
1 parent c132bdf commit 177e3d8

File tree

10 files changed

+537
-35
lines changed

10 files changed

+537
-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: 183 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

@@ -280,23 +283,28 @@ func PrepareMounts(ctx context.Context, mm *mounts.MountManager, cm cache.Manage
280283
return p, nil
281284
}
282285

286+
type gatewayMount struct {
287+
mounter snapshot.Mounter
288+
}
289+
283290
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)
291+
id string
292+
netMode opspb.NetMode
293+
hostname string
294+
extraHosts []executor.HostIP
295+
platform *opspb.Platform
296+
rootFS executor.Mount
297+
mounts []executor.Mount
298+
executor executor.Executor
299+
sm *session.Manager
300+
group session.Group
301+
started bool
302+
errGroup *errgroup.Group
303+
mu sync.Mutex
304+
cleanup []func() error
305+
ctx context.Context
306+
cancel func(error)
307+
localMounts map[executor.Mount]fs.FS
300308
}
301309

302310
func (gwCtr *gatewayContainer) Start(ctx context.Context, req client.StartRequest) (client.ContainerProcess, error) {
@@ -419,6 +427,124 @@ func (gwCtr *gatewayContainer) Release(ctx context.Context) error {
419427
return stack.Enable(err2)
420428
}
421429

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

0 commit comments

Comments
 (0)