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 <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2026-02-05 09:40:55 -06:00
parent 44945d71ff
commit 1da36d13e8
2 changed files with 100 additions and 7 deletions
+13
View File
@@ -11,6 +11,7 @@ import (
gateway "github.com/moby/buildkit/frontend/gateway/client" gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/tonistiigi/fsutil/types"
) )
type InvokeConfig struct { type InvokeConfig struct {
@@ -145,6 +146,18 @@ func (c *Container) Exec(ctx context.Context, cfg *InvokeConfig, stdin io.ReadCl
return err 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 { 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) processCfg, err := resultCtx.getProcessConfig(cfg, stdin, stdout, stderr)
if err != nil { if err != nil {
+87 -7
View File
@@ -12,10 +12,12 @@ import (
"github.com/google/go-dap" "github.com/google/go-dap"
"github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb"
gateway "github.com/moby/buildkit/frontend/gateway/client" 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/errdefs"
"github.com/moby/buildkit/solver/pb" "github.com/moby/buildkit/solver/pb"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/tonistiigi/fsutil/types"
"golang.org/x/sync/errgroup" "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.paused = make(chan stepType, 1)
t.prepareResultHandle(c, k, refs, err)
ctx, cancel := context.WithCancelCause(c) ctx, cancel := context.WithCancelCause(c)
t.collectStackTrace(ctx, pos, refs) t.collectStackTrace(ctx, pos, refs)
t.cancel = cancel 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 event.ThreadId = t.id
c.C() <- &dap.StoppedEvent{ c.C() <- &dap.StoppedEvent{
Event: dap.Event{Event: "stopped"}, 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 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 // Create a context for cancellations and make the cancel function
// block on the wait group. // block on the wait group.
var wg sync.WaitGroup 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) 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 // Start the attach. Use the context we created and perform it in
// a goroutine. We aren't necessarily assuming this will actually work. // a goroutine. We aren't necessarily assuming this will actually work.
wg.Go(func() { 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 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,
})
}