Skip to content

Commit f18bd40

Browse files
committed
monitor: add debug-shell and on-error
Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>
1 parent f2ac30f commit f18bd40

File tree

20 files changed

+1016
-274
lines changed

20 files changed

+1016
-274
lines changed

build/build.go

Lines changed: 290 additions & 83 deletions
Large diffs are not rendered by default.

commands/build.go

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/docker/buildx/controller"
1717
cbuild "github.com/docker/buildx/controller/build"
1818
"github.com/docker/buildx/controller/control"
19+
controllererrors "github.com/docker/buildx/controller/errdefs"
1920
controllerapi "github.com/docker/buildx/controller/pb"
2021
"github.com/docker/buildx/monitor"
2122
"github.com/docker/buildx/store"
@@ -488,15 +489,7 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er
488489
}
489490
}()
490491

491-
f := ioset.NewSingleForwarder()
492-
pr, pw := io.Pipe()
493-
f.SetWriter(pw, func() io.WriteCloser {
494-
pw.Close() // propagate EOF
495-
logrus.Debug("propagating stdin close")
496-
return nil
497-
})
498-
f.SetReader(os.Stdin)
499-
492+
// Start build
500493
opts, err := options.toControllerOptions()
501494
if err != nil {
502495
return err
@@ -506,38 +499,59 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er
506499
return err
507500
}
508501

509-
// Avoid leaving a stale file if we eventually fail
510-
if options.imageIDFile != "" {
511-
if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) {
512-
return errors.Wrap(err, "removing image ID file")
502+
var ref string
503+
var retErr error
504+
f := ioset.NewSingleForwarder()
505+
f.SetReader(os.Stdin)
506+
if options.invoke != "debug-shell" {
507+
pr, pw := io.Pipe()
508+
f.SetWriter(pw, func() io.WriteCloser {
509+
pw.Close() // propagate EOF
510+
logrus.Debug("propagating stdin close")
511+
return nil
512+
})
513+
514+
// Avoid leaving a stale file if we eventually fail
515+
if options.imageIDFile != "" {
516+
if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) {
517+
return errors.Wrap(err, "removing image ID file")
518+
}
513519
}
514-
}
515520

516-
// Start build
517-
ref, resp, err := c.Build(ctx, opts, pr, os.Stdout, os.Stderr, progress)
518-
if err != nil {
519-
return errors.Wrapf(err, "failed to build") // TODO: allow invoke even on error
520-
}
521-
if err := pw.Close(); err != nil {
522-
logrus.Debug("failed to close stdin pipe writer")
523-
}
524-
if err := pr.Close(); err != nil {
525-
logrus.Debug("failed to close stdin pipe reader")
526-
}
521+
var resp *client.SolveResponse
522+
ref, resp, err = c.Build(ctx, opts, pr, os.Stdout, os.Stderr, progress)
523+
if err != nil {
524+
var be *controllererrors.BuildError
525+
if errors.As(err, &be) {
526+
ref = be.Ref
527+
retErr = err
528+
// We can proceed to monitor
529+
} else {
530+
return errors.Wrapf(err, "failed to build")
531+
}
532+
}
533+
if err := pw.Close(); err != nil {
534+
logrus.Debug("failed to close stdin pipe writer")
535+
}
536+
if err := pr.Close(); err != nil {
537+
logrus.Debug("failed to close stdin pipe reader")
538+
}
527539

528-
if options.quiet {
529-
fmt.Println(resp.ExporterResponse[exptypes.ExporterImageDigestKey])
530-
}
531-
if options.imageIDFile != "" {
532-
dgst := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
533-
if v, ok := resp.ExporterResponse[exptypes.ExporterImageConfigDigestKey]; ok {
534-
dgst = v
540+
if options.quiet {
541+
fmt.Println(resp.ExporterResponse[exptypes.ExporterImageDigestKey])
542+
}
543+
if options.imageIDFile != "" {
544+
dgst := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
545+
if v, ok := resp.ExporterResponse[exptypes.ExporterImageConfigDigestKey]; ok {
546+
dgst = v
547+
}
548+
return os.WriteFile(options.imageIDFile, []byte(dgst), 0644)
535549
}
536-
return os.WriteFile(options.imageIDFile, []byte(dgst), 0644)
550+
537551
}
538552

539553
// post-build operations
540-
if options.invoke != "" {
554+
if needsMonitor(options.invoke, retErr) {
541555
pr2, pw2 := io.Pipe()
542556
f.SetWriter(pw2, func() io.WriteCloser {
543557
pw2.Close() // propagate EOF
@@ -550,7 +564,7 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er
550564
}
551565
return errors.Errorf("failed to configure terminal: %v", err)
552566
}
553-
err = monitor.RunMonitor(ctx, ref, opts, invokeConfig, c, options.progress, pr2, os.Stdout, os.Stderr)
567+
err = monitor.RunMonitor(ctx, ref, &opts, invokeConfig, c, progress, pr2, os.Stdout, os.Stderr)
554568
con.Reset()
555569
if err := pw2.Close(); err != nil {
556570
logrus.Debug("failed to close monitor stdin pipe reader")
@@ -566,9 +580,26 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er
566580
return nil
567581
}
568582

583+
func needsMonitor(invokeFlag string, retErr error) bool {
584+
switch invokeFlag {
585+
case "debug-shell":
586+
return true
587+
case "on-error":
588+
return retErr != nil
589+
default:
590+
return invokeFlag != ""
591+
}
592+
}
593+
569594
func parseInvokeConfig(invoke string) (cfg controllerapi.ContainerConfig, err error) {
570595
cfg.Tty = true
571-
if invoke == "default" {
596+
switch invoke {
597+
case "default", "debug-shell":
598+
return cfg, nil
599+
case "on-error":
600+
// NOTE: we overwrite the command to run because the original one should fail on the failed step.
601+
// TODO: make this configurable.
602+
cfg.Cmd = []string{"/bin/sh"}
572603
return cfg, nil
573604
}
574605

commands/debug-shell.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"os"
6+
"runtime"
7+
8+
"github.com/containerd/console"
9+
"github.com/docker/buildx/controller"
10+
"github.com/docker/buildx/controller/control"
11+
controllerapi "github.com/docker/buildx/controller/pb"
12+
"github.com/docker/buildx/monitor"
13+
"github.com/docker/cli/cli/command"
14+
"github.com/pkg/errors"
15+
"github.com/sirupsen/logrus"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
func debugShellCmd(dockerCli command.Cli) *cobra.Command {
20+
var options control.ControlOptions
21+
var progress string
22+
23+
cmd := &cobra.Command{
24+
Use: "debug-shell",
25+
Short: "Start a monitor",
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
ctx := context.TODO()
28+
c, err := controller.NewController(ctx, options, dockerCli)
29+
if err != nil {
30+
return err
31+
}
32+
defer func() {
33+
if err := c.Close(); err != nil {
34+
logrus.Warnf("failed to close server connection %v", err)
35+
}
36+
}()
37+
con := console.Current()
38+
if err := con.SetRaw(); err != nil {
39+
return errors.Errorf("failed to configure terminal: %v", err)
40+
}
41+
err = monitor.RunMonitor(ctx, "", nil, controllerapi.ContainerConfig{
42+
Tty: true,
43+
}, c, progress, os.Stdin, os.Stdout, os.Stderr)
44+
con.Reset()
45+
return err
46+
},
47+
}
48+
49+
flags := cmd.Flags()
50+
51+
flags.StringVar(&options.Root, "root", "", "Specify root directory of server to connect [experimental]")
52+
flags.BoolVar(&options.Detach, "detach", runtime.GOOS == "linux", "Detach buildx server (supported only on linux) [experimental]")
53+
flags.StringVar(&options.ServerConfig, "server-config", "", "Specify buildx server config file (used only when launching new server) [experimental]")
54+
flags.StringVar(&progress, "progress", "auto", `Set type of progress output ("auto", "plain", "tty"). Use plain to show container output`)
55+
56+
return cmd
57+
}
58+
59+
func addDebugShellCommand(cmd *cobra.Command, dockerCli command.Cli) {
60+
cmd.AddCommand(
61+
debugShellCmd(dockerCli),
62+
)
63+
}

commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func addCommands(cmd *cobra.Command, dockerCli command.Cli) {
8989
)
9090
if isExperimental() {
9191
remote.AddControllerCommands(cmd, dockerCli)
92+
addDebugShellCommand(cmd, dockerCli)
9293
}
9394
}
9495

controller/build/build.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/moby/buildkit/client"
3232
"github.com/moby/buildkit/session/auth/authprovider"
3333
"github.com/moby/buildkit/solver/errdefs"
34+
solverpb "github.com/moby/buildkit/solver/pb"
3435
"github.com/moby/buildkit/util/grpcerrors"
3536
"github.com/moby/buildkit/util/progress/progressui"
3637
"github.com/morikuni/aec"
@@ -209,6 +210,9 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, ng *store.NodeGrou
209210
err = err1
210211
}
211212
if err != nil {
213+
if res != nil {
214+
err = wrapResultContext(err, res)
215+
}
212216
return nil, nil, err
213217
}
214218

@@ -379,3 +383,49 @@ func controllerUlimitOpt2DockerUlimit(u *controllerapi.UlimitOpt) *dockeropts.Ul
379383
}
380384
return dockeropts.NewUlimitOpt(&values)
381385
}
386+
387+
type ResultContextError struct {
388+
ResultContext *build.ResultContext
389+
error
390+
}
391+
392+
func (e *ResultContextError) Unwrap() error {
393+
return e.error
394+
}
395+
396+
func wrapResultContext(wErr error, res *build.ResultContext) error {
397+
if wErr == nil {
398+
return nil
399+
}
400+
def, err := DefinitionFromResultContext(context.TODO(), res)
401+
if err != nil {
402+
logrus.Errorf("failed to get definition from result: %v", err)
403+
return wErr
404+
}
405+
res2, err := build.GetResultAt(context.TODO(), res, def, nil)
406+
if err != nil {
407+
logrus.Errorf("failed to get result: %v", err)
408+
return wErr
409+
}
410+
res.Done()
411+
return &ResultContextError{ResultContext: res2, error: wErr}
412+
}
413+
414+
func DefinitionFromResultContext(ctx context.Context, res *build.ResultContext) (*solverpb.Definition, error) {
415+
if res.Res == nil {
416+
return nil, errors.Errorf("result context doesn't contain build result")
417+
}
418+
ref, err := res.Res.SingleRef()
419+
if err != nil {
420+
return nil, err
421+
}
422+
st, err := ref.ToState()
423+
if err != nil {
424+
return nil, err
425+
}
426+
def, err := st.Marshal(ctx)
427+
if err != nil {
428+
return nil, err
429+
}
430+
return def.ToPB(), nil
431+
}

controller/control/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type BuildxController interface {
1616
Close() error
1717
List(ctx context.Context) (refs []string, _ error)
1818
Disconnect(ctx context.Context, ref string) error
19+
Inspect(ctx context.Context, ref string) (*controllerapi.InspectResponse, error)
1920
}
2021

2122
type ControlOptions struct {

controller/errdefs/build.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package errdefs
2+
3+
import (
4+
"github.com/containerd/typeurl"
5+
"github.com/moby/buildkit/util/grpcerrors"
6+
)
7+
8+
func init() {
9+
typeurl.Register((*Build)(nil), "github.com/docker/buildx", "errdefs.Build+json")
10+
}
11+
12+
type BuildError struct {
13+
Build
14+
error
15+
}
16+
17+
func (e *BuildError) Unwrap() error {
18+
return e.error
19+
}
20+
21+
func (e *BuildError) ToProto() grpcerrors.TypedErrorProto {
22+
return &e.Build
23+
}
24+
25+
func WrapBuild(err error, ref string) error {
26+
if err == nil {
27+
return nil
28+
}
29+
return &BuildError{Build: Build{Ref: ref}, error: err}
30+
}
31+
32+
func (b *Build) WrapError(err error) error {
33+
return &BuildError{error: err, Build: *b}
34+
}

0 commit comments

Comments
 (0)