From 1da36d13e841f15ec9a3ca5d8aca70db64570349 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Thu, 5 Feb 2026 09:40:55 -0600 Subject: [PATCH] dap: use container fs requests to get a more accurate state for the file system The container filesystem request API that has been added to buildkit allows a container created through the `NewContainer` API to also access the filesystems. This is useful when an error occurs because it allows us to grab the mutable state of the mounts used during the actual build rather than the input version copies which don't contain any files that were added as part of the failed command. This gives us a more accurate view of the filesystem that was previously only accessible through using `exec` and `ls`/`cat` commands that may not always exist. Signed-off-by: Jonathan A. Sternberg --- build/invoke.go | 13 +++++++ dap/thread.go | 94 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/build/invoke.go b/build/invoke.go index 70df846d7ea7..0b65fb08ca5e 100644 --- a/build/invoke.go +++ b/build/invoke.go @@ -11,6 +11,7 @@ import ( gateway "github.com/moby/buildkit/frontend/gateway/client" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tonistiigi/fsutil/types" ) type InvokeConfig struct { @@ -145,6 +146,18 @@ func (c *Container) Exec(ctx context.Context, cfg *InvokeConfig, stdin io.ReadCl return err } +func (c *Container) ReadFile(ctx context.Context, req gateway.ReadContainerRequest) ([]byte, error) { + return c.container.ReadFile(ctx, req) +} + +func (c *Container) ReadDir(ctx context.Context, req gateway.ReadDirContainerRequest) ([]*types.Stat, error) { + return c.container.ReadDir(ctx, req) +} + +func (c *Container) StatFile(ctx context.Context, req gateway.StatContainerRequest) (*types.Stat, error) { + return c.container.StatFile(ctx, req) +} + func exec(ctx context.Context, resultCtx *ResultHandle, cfg *InvokeConfig, ctr gateway.Container, stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error { processCfg, err := resultCtx.getProcessConfig(cfg, stdin, stdout, stderr) if err != nil { diff --git a/dap/thread.go b/dap/thread.go index febd4677e8fa..225c765a1823 100644 --- a/dap/thread.go +++ b/dap/thread.go @@ -12,10 +12,12 @@ import ( "github.com/google/go-dap" "github.com/moby/buildkit/client/llb" gateway "github.com/moby/buildkit/frontend/gateway/client" + gwpb "github.com/moby/buildkit/frontend/gateway/pb" "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/solver/pb" "github.com/opencontainers/go-digest" "github.com/pkg/errors" + "github.com/tonistiigi/fsutil/types" "golang.org/x/sync/errgroup" ) @@ -323,16 +325,12 @@ func (t *thread) pause(c Context, k string, refs map[string]gateway.Reference, e } t.paused = make(chan stepType, 1) + t.prepareResultHandle(c, k, refs, err) + ctx, cancel := context.WithCancelCause(c) t.collectStackTrace(ctx, pos, refs) t.cancel = cancel - // Used for exec. Only works if there was an error or if the step returns - // a root mount. - if ref, ok := refs[k]; ok || err != nil { - t.prepareResultHandle(c, ref, err) - } - event.ThreadId = t.id c.C() <- &dap.StoppedEvent{ Event: dap.Event{Event: "stopped"}, @@ -341,7 +339,15 @@ func (t *thread) pause(c Context, k string, refs map[string]gateway.Reference, e return t.paused } -func (t *thread) prepareResultHandle(c Context, ref gateway.Reference, err error) { +func (t *thread) prepareResultHandle(c Context, k string, refs map[string]gateway.Reference, err error) { + var ref gateway.Reference + if err == nil { + var ok bool + if ref, ok = refs[k]; !ok { + return + } + } + // Create a context for cancellations and make the cancel function // block on the wait group. var wg sync.WaitGroup @@ -353,6 +359,31 @@ func (t *thread) prepareResultHandle(c Context, ref gateway.Reference, err error t.rCtx = build.NewResultHandle(ctx, t.c, ref, t.meta, err) + if err != nil { + gwcaps := t.c.BuildOpts().Caps + + var solveErr *errdefs.SolveError + // If we had a solve error and the exec filesystem capability, we can + // get the filesystem mounts used in the actual build rather than only the input + // mounts. + if gwcaps.Supports(gwpb.CapGatewayExecFilesystem) == nil && errors.As(err, &solveErr) { + if exec, ok := solveErr.Op.Op.(*pb.Op_Exec); ok { + rCtx := t.rCtx + + getContainer := sync.OnceValues(func() (*build.Container, error) { + return build.NewContainer(c, rCtx, &build.InvokeConfig{}) + }) + + for i, m := range exec.Exec.Mounts { + refs[m.Dest] = &mountReference{ + getContainer: getContainer, + index: i, + } + } + } + } + } + // Start the attach. Use the context we created and perform it in // a goroutine. We aren't necessarily assuming this will actually work. wg.Go(func() { @@ -678,3 +709,52 @@ func (t *thread) rewind(ctx Context, inErr error) (k string, result *step, mount } return k, result, mounts, inErr } + +type mountReference struct { + getContainer func() (*build.Container, error) + index int +} + +func (r *mountReference) ToState() (llb.State, error) { + return llb.State{}, errors.New("unimplemented, cannot use ToState with mount reference") +} + +func (r *mountReference) Evaluate(ctx context.Context) error { + return nil +} + +func (r *mountReference) ReadFile(ctx context.Context, req gateway.ReadRequest) ([]byte, error) { + ctr, err := r.getContainer() + if err != nil { + return nil, err + } + + return ctr.ReadFile(ctx, gateway.ReadContainerRequest{ + ReadRequest: req, + MountIndex: r.index, + }) +} + +func (r *mountReference) StatFile(ctx context.Context, req gateway.StatRequest) (*types.Stat, error) { + ctr, err := r.getContainer() + if err != nil { + return nil, err + } + + return ctr.StatFile(ctx, gateway.StatContainerRequest{ + StatRequest: req, + MountIndex: r.index, + }) +} + +func (r *mountReference) ReadDir(ctx context.Context, req gateway.ReadDirRequest) ([]*types.Stat, error) { + ctr, err := r.getContainer() + if err != nil { + return nil, err + } + + return ctr.ReadDir(ctx, gateway.ReadDirContainerRequest{ + ReadDirRequest: req, + MountIndex: r.index, + }) +}