package commands import ( "encoding/json" "io" "os" "strconv" "strings" "github.com/docker/buildx/build" "github.com/docker/buildx/monitor" "github.com/docker/buildx/util/cobrautil" "github.com/docker/buildx/util/ioset" "github.com/docker/buildx/util/progress" "github.com/docker/cli/cli/command" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/tonistiigi/go-csvvalue" ) type debugOptions struct { // InvokeFlag is a flag to configure the launched debugger and the commaned executed on the debugger. InvokeFlag string // OnFlag is a flag to configure the timing of launching the debugger. OnFlag string } // debuggerOptions will start a debuggerOptions instance. type debuggerOptions interface { New(in ioset.In) (debuggerInstance, error) } // debuggerInstance is an instance of a Debugger that has been started. type debuggerInstance interface { Start(printer *progress.Printer, opts *BuildOptions) error Handler() build.Handler Stop() error Out() io.Writer } func debugCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { var options debugOptions cmd := &cobra.Command{ Use: "debug", Short: "Start debugger", DisableFlagsInUseLine: true, } cobrautil.MarkCommandExperimental(cmd) flags := cmd.Flags() flags.StringVar(&options.InvokeFlag, "invoke", "", "Launch a monitor with executing specified command") flags.StringVar(&options.OnFlag, "on", "error", "When to launch the monitor ([always, error])") cobrautil.MarkFlagsExperimental(flags, "invoke", "on") cmd.AddCommand(buildCmd(dockerCli, rootOpts, &options)) return cmd } func (d *debugOptions) New(in ioset.In) (debuggerInstance, error) { cfg, err := parseInvokeConfig(d.InvokeFlag, d.OnFlag) if err != nil { return nil, err } return &monitorDebuggerInstance{ cfg: cfg, in: in.Stdin, }, nil } type monitorDebuggerInstance struct { cfg *build.InvokeConfig in io.ReadCloser m *monitor.Monitor } func (d *monitorDebuggerInstance) Start(printer *progress.Printer, opts *BuildOptions) error { d.m = monitor.New(d.cfg, d.in, os.Stdout, os.Stderr, printer) return nil } func (d *monitorDebuggerInstance) Handler() build.Handler { return d.m.Handler() } func (d *monitorDebuggerInstance) Stop() error { return d.m.Close() } func (d *monitorDebuggerInstance) Out() io.Writer { return os.Stderr } func parseInvokeConfig(invoke, on string) (*build.InvokeConfig, error) { cfg := &build.InvokeConfig{} switch on { case "always": cfg.SuspendOn = build.SuspendAlways case "error": cfg.SuspendOn = build.SuspendError default: if invoke != "" { cfg.SuspendOn = build.SuspendAlways } } cfg.Tty = true cfg.NoCmd = true switch invoke { case "default", "": return cfg, nil case "on-error": // 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 cfg.Cmd = []string{"/bin/sh"} cfg.NoCmd = false return cfg, nil } csvParser := csvvalue.NewParser() csvParser.LazyQuotes = true fields, err := csvParser.Fields(invoke, nil) if err != nil { return nil, err } if len(fields) == 1 && !strings.Contains(fields[0], "=") { cfg.Cmd = []string{fields[0]} cfg.NoCmd = false return cfg, nil } cfg.NoUser = true cfg.NoCwd = true for _, field := range fields { parts := strings.SplitN(field, "=", 2) if len(parts) != 2 { return nil, errors.Errorf("invalid value %s", field) } key := strings.ToLower(parts[0]) value := parts[1] switch key { case "args": cfg.Cmd = append(cfg.Cmd, maybeJSONArray(value)...) cfg.NoCmd = false case "entrypoint": cfg.Entrypoint = append(cfg.Entrypoint, maybeJSONArray(value)...) if cfg.Cmd == nil { cfg.Cmd = []string{} cfg.NoCmd = false } case "env": cfg.Env = append(cfg.Env, maybeJSONArray(value)...) case "user": cfg.User = value cfg.NoUser = false case "cwd": cfg.Cwd = value cfg.NoCwd = false case "tty": cfg.Tty, err = strconv.ParseBool(value) if err != nil { return nil, errors.Errorf("failed to parse tty: %v", err) } default: return nil, errors.Errorf("unknown key %q", key) } } return cfg, nil } func maybeJSONArray(v string) []string { var list []string if err := json.Unmarshal([]byte(v), &list); err == nil { return list } return []string{v} }