Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

monitor: breakpoint debugger on monitor and on IDEs (via DAP) #1656

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 8 additions & 4 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,10 +676,10 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt Op
}

func Build(ctx context.Context, nodes []builder.Node, opt map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer) (resp map[string]*client.SolveResponse, err error) {
return BuildWithResultHandler(ctx, nodes, opt, docker, configDir, w, nil)
return BuildWithResultHandler(ctx, nodes, opt, docker, configDir, w, nil, false)
}

func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultHandle)) (resp map[string]*client.SolveResponse, err error) {
func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultHandle) error, noEval bool) (resp map[string]*client.SolveResponse, err error) {
if len(nodes) == 0 {
return nil, errors.Errorf("driver required for build")
}
Expand Down Expand Up @@ -949,8 +949,12 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
var rr *client.SolveResponse
if resultHandleFunc != nil {
var resultHandle *ResultHandle
resultHandle, rr, err = NewResultHandle(ctx, cc, so, "buildx", buildFunc, ch)
resultHandleFunc(dp.driverIndex, resultHandle)
resultHandle, rr, err = NewResultHandle(ctx, cc, so, "buildx", buildFunc, ch, noEval)
if err == nil {
if err := resultHandleFunc(dp.driverIndex, resultHandle); err != nil {
return err
}
}
} else {
rr, err = c.Build(ctx, so, "buildx", buildFunc, ch)
}
Expand Down
92 changes: 85 additions & 7 deletions build/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
gateway "github.com/moby/buildkit/frontend/gateway/client"
Expand All @@ -28,7 +29,7 @@ import (
// failures and successes.
//
// If the returned ResultHandle is not nil, the caller must call Done() on it.
func NewResultHandle(ctx context.Context, cc *client.Client, opt client.SolveOpt, product string, buildFunc gateway.BuildFunc, ch chan *client.SolveStatus) (*ResultHandle, *client.SolveResponse, error) {
func NewResultHandle(ctx context.Context, cc *client.Client, opt client.SolveOpt, product string, buildFunc gateway.BuildFunc, ch chan *client.SolveStatus, noEval bool) (*ResultHandle, *client.SolveResponse, error) {
// Create a new context to wrap the original, and cancel it when the
// caller-provided context is cancelled.
//
Expand Down Expand Up @@ -85,19 +86,43 @@ func NewResultHandle(ctx context.Context, cc *client.Client, opt client.SolveOpt
defer cancel(context.Canceled) // ensure no dangling processes

var res *gateway.Result
var singleDef *pb.Definition
var err error
resp, err = cc.Build(ctx, opt, product, func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
var err error
res, err = buildFunc(ctx, c)

if res != nil && err == nil {
// Force evaluation of the build result (otherwise, we likely
// won't get a solve error)
def, err2 := getDefinition(ctx, res)
if err2 != nil {
return nil, err2
var defErr error
singleDef, defErr = getSingleDefinition(ctx, res) // TODO: support multi-platform
if defErr != nil {
return nil, defErr
}
if noEval {
respHandle = &ResultHandle{
client: cc,
solveOpt: opt,
def: singleDef,
done: make(chan struct{}),
gwClient: c,
gwCtx: ctx,
}
close(done)

// Block until the caller closes the ResultHandle.
select {
case <-respHandle.done:
case <-ctx.Done():
}
} else {
// Force evaluation of the build result (otherwise, we likely
// won't get a solve error)
def, err2 := getDefinition(ctx, res)
if err2 != nil {
return nil, err2
}
res, err = evalDefinition(ctx, c, def)
}
res, err = evalDefinition(ctx, c, def)
}

if err != nil {
Expand All @@ -112,6 +137,9 @@ func NewResultHandle(ctx context.Context, cc *client.Client, opt client.SolveOpt
var se *errdefs.SolveError
if errors.As(err, &se) {
respHandle = &ResultHandle{
client: cc,
solveOpt: opt,
def: singleDef,
done: make(chan struct{}),
solveErr: se,
gwClient: c,
Expand Down Expand Up @@ -169,6 +197,9 @@ func NewResultHandle(ctx context.Context, cc *client.Client, opt client.SolveOpt
return nil, errors.Wrap(err, "inconsistent solve result")
}
respHandle = &ResultHandle{
client: cc,
solveOpt: opt,
def: singleDef,
done: make(chan struct{}),
res: res,
gwClient: c,
Expand Down Expand Up @@ -251,8 +282,51 @@ func evalDefinition(ctx context.Context, c gateway.Client, defs *result.Result[*
return res, nil
}

func getSingleDefinition(ctx context.Context, res *gateway.Result) (*pb.Definition, error) {
defs, err := getDefinition(ctx, res)
if err != nil {
return nil, err
}
ps, err := exptypes.ParsePlatforms(res.Metadata)
if err != nil {
return nil, err
}
def, ok := defs.FindRef(ps.Platforms[0].ID)
if !ok {
return nil, errors.Errorf("no reference found")
}
return def, nil
}

func DefinitionFromResultHandler(ctx context.Context, res *ResultHandle) (*pb.Definition, error) {
if res.def != nil {
return res.def, nil
}
return nil, errors.Errorf("result context doesn't contain build definition")
}

func SolveWithResultHandler(ctx context.Context, product string, resultCtx *ResultHandle, target *pb.Definition, pw progress.Writer) (*ResultHandle, error) {
opt := resultCtx.solveOpt
opt.Ref = ""
opt.Exports = nil
opt.CacheExports = nil
ch, done := progress.NewChannel(pw)
defer func() { <-done }()
h, _, err := NewResultHandle(ctx, resultCtx.client, opt, product, func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
return c.Solve(ctx, gateway.SolveRequest{
Evaluate: true,
Definition: target,
})
}, ch, false)
return h, err
}

// ResultHandle is a build result with the client that built it.
type ResultHandle struct {
client *client.Client
solveOpt client.SolveOpt
def *pb.Definition

res *gateway.Result
solveErr *errdefs.SolveError

Expand Down Expand Up @@ -281,6 +355,10 @@ func (r *ResultHandle) Done() {
})
}

func (r *ResultHandle) SolveError() *errdefs.SolveError {
return r.solveErr
}

func (r *ResultHandle) registerCleanup(f func()) {
r.cleanupsMu.Lock()
r.cleanups = append(r.cleanups, f)
Expand Down
7 changes: 6 additions & 1 deletion commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,11 @@ func runControllerBuild(ctx context.Context, dockerCli command.Cli, opts *contro
return nil
})

if options.invokeConfig != nil && options.invokeConfig.invokeFlag == "debug-step" {
// Special mode where we don't get the result but get only the build definition.
// In this mode, Build() doesn't perform the build therefore always fails.
opts.Debug = true
}
ref, resp, err = c.Build(ctx, *opts, pr, printer)
if err != nil {
var be *controllererrors.BuildError
Expand Down Expand Up @@ -862,7 +867,7 @@ func (cfg *invokeConfig) parseInvokeConfig(invoke, on string) error {
switch invoke {
case "default", "":
return nil
case "on-error":
case "on-error", "debug-step":
// NOTE: we overwrite the command to run because the original one should fail on the failed step.
// TODO: make this configurable via flags or restorable from LLB.
// Discussion: https://github.com/docker/buildx/pull/1640#discussion_r1113295900
Expand Down
57 changes: 57 additions & 0 deletions commands/dap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package commands

import (
"fmt"
"os"

"github.com/docker/buildx/monitor/dap"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func addDAPCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(
dapCmd(dockerCli),
attachContainerCmd(dockerCli),
)
}

func dapCmd(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "dap",
Short: "Debug Adapter Protocol server",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
logrus.SetOutput(os.Stderr)
s, err := dap.NewServer(dockerCli, os.Stdin, os.Stdout)
if err != nil {
return err
}
if err := s.Serve(); err != nil {
logrus.WithError(err).Warnf("failed to serve")
}
logrus.Info("finishing server")
return nil
},
}
return cmd
}

func attachContainerCmd(dockerCli command.Cli) *cobra.Command {
var setTtyRaw bool
cmd := &cobra.Command{
Use: fmt.Sprintf("%s [OPTIONS] rootdir", dap.AttachContainerCommand),
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 || args[0] == "" {
return errors.Errorf("specify root dir: %+v", args)
}
return dap.AttachContainerIO(args[0], os.Stdin, os.Stdout, os.Stderr, setTtyRaw)
},
}
flags := cmd.Flags()
flags.BoolVar(&setTtyRaw, "set-tty-raw", false, "set tty raw")
return cmd
}
1 change: 1 addition & 0 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func addCommands(cmd *cobra.Command, dockerCli command.Cli) {
"builder",
completion.BuilderNames(dockerCli),
)
addDAPCommands(cmd, dockerCli) // hidden command; we need it for emacs DAP support
}

func rootFlags(options *rootOptions, flags *pflag.FlagSet) {
Expand Down
12 changes: 8 additions & 4 deletions controller/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in controllerapi.Build
return nil, nil, err
}

resp, res, err := buildTargets(ctx, dockerCli, b.NodeGroup, nodes, map[string]build.Options{defaultTargetName: opts}, progress, generateResult)
resp, res, err := buildTargets(ctx, dockerCli, b.NodeGroup, nodes, map[string]build.Options{defaultTargetName: opts}, progress, generateResult, in.Debug)
err = wrapBuildError(err, false)
if err != nil {
// NOTE: buildTargets can return *build.ResultHandle even on error.
Expand All @@ -190,20 +190,24 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in controllerapi.Build
// NOTE: When an error happens during the build and this function acquires the debuggable *build.ResultHandle,
// this function returns it in addition to the error (i.e. it does "return nil, res, err"). The caller can
// inspect the result and debug the cause of that error.
func buildTargets(ctx context.Context, dockerCli command.Cli, ng *store.NodeGroup, nodes []builder.Node, opts map[string]build.Options, progress progress.Writer, generateResult bool) (*client.SolveResponse, *build.ResultHandle, error) {
func buildTargets(ctx context.Context, dockerCli command.Cli, ng *store.NodeGroup, nodes []builder.Node, opts map[string]build.Options, progress progress.Writer, generateResult, debug bool) (*client.SolveResponse, *build.ResultHandle, error) {
var res *build.ResultHandle
var resp map[string]*client.SolveResponse
var err error
if generateResult {
var mu sync.Mutex
var idx int
resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) {
resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) error {
mu.Lock()
defer mu.Unlock()
if res == nil || driverIndex < idx {
idx, res = driverIndex, gotRes
}
})
if debug {
return errors.Errorf("debug mode")
}
return nil
}, debug)
} else {
resp, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress)
}
Expand Down
2 changes: 2 additions & 0 deletions controller/control/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
solverpb "github.com/moby/buildkit/solver/pb"
)

type BuildxController interface {
Expand All @@ -23,6 +24,7 @@ type BuildxController interface {
ListProcesses(ctx context.Context, ref string) (infos []*controllerapi.ProcessInfo, retErr error)
DisconnectProcess(ctx context.Context, ref, pid string) error
Inspect(ctx context.Context, ref string) (*controllerapi.InspectResponse, error)
Solve(ctx context.Context, ref string, def *solverpb.Definition, progress progress.Writer) error
}

type ControlOptions struct {
Expand Down
31 changes: 30 additions & 1 deletion controller/local/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/moby/buildkit/client"
solverpb "github.com/moby/buildkit/solver/pb"
"github.com/pkg/errors"
)

Expand All @@ -40,6 +41,8 @@ type localController struct {
processes *processes.Manager

buildOnGoing atomic.Bool

originalResult *build.ResultHandle
}

func (b *localController) Build(ctx context.Context, options controllerapi.BuildOptions, in io.ReadCloser, progress progress.Writer) (string, *client.SolveResponse, error) {
Expand All @@ -55,6 +58,7 @@ func (b *localController) Build(ctx context.Context, options controllerapi.Build
resultCtx: res,
buildOptions: &options,
}
b.originalResult = res
if buildErr != nil {
buildErr = controllererrors.WrapBuild(buildErr, b.ref)
}
Expand Down Expand Up @@ -142,5 +146,30 @@ func (b *localController) Inspect(ctx context.Context, ref string) (*controllera
if ref != b.ref {
return nil, errors.Errorf("unknown ref %q", ref)
}
return &controllerapi.InspectResponse{Options: b.buildConfig.buildOptions}, nil
var curDef *solverpb.Definition
var origDef *solverpb.Definition
if b.buildConfig.resultCtx != nil {
curDef, _ = build.DefinitionFromResultHandler(ctx, b.buildConfig.resultCtx)
}
if b.originalResult != nil {
origDef, _ = build.DefinitionFromResultHandler(ctx, b.originalResult)
}
return &controllerapi.InspectResponse{Options: b.buildConfig.buildOptions, Definition: origDef, CurrentDefinition: curDef}, nil
}

func (b *localController) Solve(ctx context.Context, ref string, target *solverpb.Definition, progress progress.Writer) error {
if ref != b.ref {
return errors.Errorf("unknown ref %q", ref)
}
if b.originalResult == nil {
return errors.Errorf("no build has been called")
}
res, err := build.SolveWithResultHandler(ctx, "buildx", b.originalResult, target, progress)
if err == nil {
b.buildConfig.resultCtx = res
if se := res.SolveError(); se != nil {
err = errors.Errorf("failed solve: %v", se)
}
}
return err
}