Skip to content

Commit 7186b8a

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 <jonathan.sternberg@docker.com>
1 parent ed6dc74 commit 7186b8a

11 files changed

Lines changed: 716 additions & 43 deletions

File tree

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ type Mount struct {
6666
type Container interface {
6767
Start(context.Context, StartRequest) (ContainerProcess, error)
6868
Release(context.Context) error
69+
ReadFile(ctx context.Context, req ReadContainerRequest) ([]byte, error)
70+
StatFile(ctx context.Context, req StatContainerRequest) (*fstypes.Stat, error)
71+
ReadDir(ctx context.Context, req ReadDirContainerRequest) ([]*fstypes.Stat, error)
6972
}
7073

7174
// StartRequest encapsulates the arguments to define a process within a
@@ -111,6 +114,11 @@ type ReadRequest struct {
111114
Range *FileRange
112115
}
113116

117+
type ReadContainerRequest struct {
118+
ReadRequest
119+
Index int
120+
}
121+
114122
type FileRange struct {
115123
Offset int
116124
Length int
@@ -121,10 +129,20 @@ type ReadDirRequest struct {
121129
IncludePattern string
122130
}
123131

132+
type ReadDirContainerRequest struct {
133+
ReadDirRequest
134+
Index int
135+
}
136+
124137
type StatRequest struct {
125138
Path string
126139
}
127140

141+
type StatContainerRequest struct {
142+
StatRequest
143+
Index int
144+
}
145+
128146
// SolveRequest is same as frontend.SolveRequest but avoiding dependency
129147
type SolveRequest struct {
130148
Evaluate bool

frontend/gateway/container/container.go

Lines changed: 202 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

@@ -104,6 +107,26 @@ func NewContainer(ctx context.Context, cm cache.Manager, exec executor.Executor,
104107
ctr.rootFS = p.Root
105108
ctr.mounts = p.Mounts
106109

110+
// Setup the local mounts. These need to be in the same order as the original
111+
// parameters.
112+
ctr.localMounts = make([]gatewayContainerMount, len(mnts))
113+
for i, m := range mnts {
114+
if m.Dest == "/" {
115+
ctr.localMounts[i].Src = p.Root.Src
116+
continue
117+
}
118+
119+
index := slices.IndexFunc(p.Mounts, func(em executor.Mount) bool {
120+
return em.Dest == m.Dest
121+
})
122+
if index < 0 {
123+
// This shouldn't happen but handle it just in case.
124+
continue
125+
}
126+
127+
ctr.localMounts[i].Src = p.Mounts[index].Src
128+
}
129+
107130
for _, o := range p.OutputRefs {
108131
ctr.cleanup = append(ctr.cleanup, func() error {
109132
return o.Ref.Release(context.TODO())
@@ -281,22 +304,23 @@ func PrepareMounts(ctx context.Context, mm *mounts.MountManager, cm cache.Manage
281304
}
282305

283306
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)
307+
id string
308+
netMode opspb.NetMode
309+
hostname string
310+
extraHosts []executor.HostIP
311+
platform *opspb.Platform
312+
rootFS executor.Mount
313+
mounts []executor.Mount
314+
executor executor.Executor
315+
sm *session.Manager
316+
group session.Group
317+
started bool
318+
errGroup *errgroup.Group
319+
mu sync.Mutex
320+
cleanup []func() error
321+
ctx context.Context
322+
cancel func(error)
323+
localMounts []gatewayContainerMount
300324
}
301325

302326
func (gwCtr *gatewayContainer) Start(ctx context.Context, req client.StartRequest) (client.ContainerProcess, error) {
@@ -419,6 +443,122 @@ func (gwCtr *gatewayContainer) Release(ctx context.Context) error {
419443
return stack.Enable(err2)
420444
}
421445

446+
func (gwCtr *gatewayContainer) ReadFile(ctx context.Context, req client.ReadContainerRequest) ([]byte, error) {
447+
fsys, err := gwCtr.mount(ctx, req.Index)
448+
if err != nil {
449+
return nil, err
450+
}
451+
452+
path, err := filepath.Rel("/", req.Filename)
453+
if err != nil {
454+
return nil, err
455+
}
456+
return fs.ReadFile(fsys, path)
457+
}
458+
459+
func (gwCtr *gatewayContainer) ReadDir(ctx context.Context, req client.ReadDirContainerRequest) ([]*fstypes.Stat, error) {
460+
fsys, err := gwCtr.mount(ctx, req.Index)
461+
if err != nil {
462+
return nil, err
463+
}
464+
465+
path, err := filepath.Rel("/", req.Path)
466+
if err != nil {
467+
return nil, err
468+
}
469+
470+
entries, err := fs.ReadDir(fsys, path)
471+
if err != nil {
472+
return nil, err
473+
}
474+
475+
files := make([]*fstypes.Stat, len(entries))
476+
for i, e := range entries {
477+
fullpath := filepath.Join(req.Path, e.Name())
478+
fi, err := e.Info()
479+
if err != nil {
480+
return nil, err
481+
}
482+
483+
files[i], err = mkstat(fsys, fullpath, e.Name(), fi)
484+
if err != nil {
485+
return nil, errors.Wrap(err, "mkstat")
486+
}
487+
}
488+
return files, nil
489+
}
490+
491+
func (gwCtr *gatewayContainer) StatFile(ctx context.Context, req client.StatContainerRequest) (*fstypes.Stat, error) {
492+
fsys, err := gwCtr.mount(ctx, req.Index)
493+
if err != nil {
494+
return nil, err
495+
}
496+
497+
path, err := filepath.Rel("/", req.Path)
498+
if err != nil {
499+
return nil, err
500+
}
501+
502+
fi, err := fs.Stat(fsys, path)
503+
if err != nil {
504+
return nil, err
505+
}
506+
return mkstat(fsys, req.Path, filepath.Base(req.Path), fi)
507+
}
508+
509+
func (gwCtr *gatewayContainer) mount(ctx context.Context, index int) (fs.FS, error) {
510+
// No lock needed for this because the number of mounts does
511+
// not change.
512+
if index < 0 || index >= len(gwCtr.localMounts) {
513+
return nil, errors.Errorf("mount index %d is out of bounds (%d available)", index, len(gwCtr.localMounts))
514+
}
515+
516+
gwCtr.mu.Lock()
517+
defer gwCtr.mu.Unlock()
518+
519+
mount := gwCtr.localMounts[index]
520+
521+
// Already mounted?
522+
if mount.FS != nil {
523+
return mount.FS, nil
524+
}
525+
526+
// Defensively check that this mount really exists.
527+
if mount.Src == nil {
528+
return nil, errors.Errorf("mountable %d not found", index)
529+
}
530+
531+
// Need to mount an instance.
532+
ref, err := mount.Src.Mount(ctx, true)
533+
if err != nil {
534+
return nil, err
535+
}
536+
537+
mounter := snapshot.LocalMounter(ref)
538+
dir, err := mounter.Mount()
539+
if err != nil {
540+
return nil, err
541+
}
542+
543+
// Register cleanup.
544+
gwCtr.cleanup = append(gwCtr.cleanup, func() error {
545+
return mounter.Unmount()
546+
})
547+
548+
root, err := os.OpenRoot(dir)
549+
if err != nil {
550+
return nil, err
551+
}
552+
553+
gwCtr.cleanup = append(gwCtr.cleanup, func() error {
554+
return root.Close()
555+
})
556+
557+
f := root.FS()
558+
gwCtr.localMounts[index].FS = f
559+
return f, nil
560+
}
561+
422562
type gatewayContainerProcess struct {
423563
errGroup *errgroup.Group
424564
groupCtx context.Context
@@ -511,3 +651,49 @@ type mountable struct {
511651
func (m *mountable) Mount(ctx context.Context, readonly bool) (snapshot.Mountable, error) {
512652
return m.m.Mount(ctx, readonly, m.g)
513653
}
654+
655+
// constructs a Stat object. path is where the path can be found right
656+
// now, relpath is the desired path to be recorded in the stat (so
657+
// relative to whatever base dir is relevant). fi is the os.Stat
658+
// info. inodemap is used to calculate hardlinks over a series of
659+
// mkstat calls and maps inode to the canonical (aka "first") path for
660+
// a set of hardlinks to that inode.
661+
func mkstat(fsys fs.FS, path, relpath string, fi os.FileInfo) (*fstypes.Stat, error) {
662+
relpath = filepath.ToSlash(relpath)
663+
664+
stat := &fstypes.Stat{
665+
Path: filepath.FromSlash(relpath),
666+
Mode: uint32(fi.Mode()),
667+
ModTime: fi.ModTime().UnixNano(),
668+
}
669+
670+
if !fi.IsDir() {
671+
stat.Size = fi.Size()
672+
if fi.Mode()&os.ModeSymlink != 0 {
673+
link, err := fs.ReadLink(fsys, path)
674+
if err != nil {
675+
return nil, errors.WithStack(err)
676+
}
677+
stat.Linkname = link
678+
}
679+
}
680+
681+
if runtime.GOOS == "windows" {
682+
permPart := stat.Mode & uint32(os.ModePerm)
683+
noPermPart := stat.Mode &^ uint32(os.ModePerm)
684+
// Add the x bit: make everything +x from windows
685+
permPart |= 0o111
686+
permPart &= 0o755
687+
stat.Mode = noPermPart | permPart
688+
}
689+
690+
// Clear the socket bit since archive/tar.FileInfoHeader does not handle it
691+
stat.Mode &^= uint32(os.ModeSocket)
692+
693+
return stat, nil
694+
}
695+
696+
type gatewayContainerMount struct {
697+
Src executor.Mountable
698+
FS fs.FS
699+
}

0 commit comments

Comments
 (0)