diff --git a/build/build.go b/build/build.go index 27e5be15..5609377b 100644 --- a/build/build.go +++ b/build/build.go @@ -709,10 +709,10 @@ func Invoke(ctx context.Context, cfg ContainerConfig) error { } func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, docker DockerAPI, configDir string, w progress.Writer) (resp map[string]*client.SolveResponse, err error) { - return BuildWithResultHandler(ctx, drivers, opt, docker, configDir, w, nil) + return BuildWithResultHandler(ctx, drivers, opt, docker, configDir, w, nil, false) } -func BuildWithResultHandler(ctx context.Context, drivers []DriverInfo, opt map[string]Options, docker DockerAPI, configDir string, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultContext)) (resp map[string]*client.SolveResponse, err error) { +func BuildWithResultHandler(ctx context.Context, drivers []DriverInfo, opt map[string]Options, docker DockerAPI, configDir string, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultContext), allowNoOutput bool) (resp map[string]*client.SolveResponse, err error) { if len(drivers) == 0 { return nil, errors.Errorf("driver required for build") } @@ -737,7 +737,7 @@ func BuildWithResultHandler(ctx context.Context, drivers []DriverInfo, opt map[s noOutputTargets = append(noOutputTargets, name) } } - if len(noOutputTargets) > 0 { + if len(noOutputTargets) > 0 && !allowNoOutput { var warnNoOutputBuf bytes.Buffer warnNoOutputBuf.WriteString("No output specified ") if len(noOutputTargets) == 1 && noOutputTargets[0] == "default" { diff --git a/commands/build.go b/commands/build.go index e3dd19da..9a1fd850 100644 --- a/commands/build.go +++ b/commands/build.go @@ -233,7 +233,7 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { contextPathHash = in.contextPath } - imageID, res, err := buildTargets(ctx, dockerCli, map[string]build.Options{defaultTargetName: opts}, in.progress, contextPathHash, in.builder, in.metadataFile) + imageID, res, err := buildTargets(ctx, dockerCli, map[string]build.Options{defaultTargetName: opts}, in.progress, contextPathHash, in.builder, in.metadataFile, in.invoke != "") err = wrapBuildError(err, false) if err != nil { return err @@ -250,7 +250,7 @@ func runBuild(dockerCli command.Cli, in buildOptions) (err error) { return errors.Errorf("failed to configure terminal: %v", err) } err = monitor.RunMonitor(ctx, cfg, func(ctx context.Context) (*build.ResultContext, error) { - _, rr, err := buildTargets(ctx, dockerCli, map[string]build.Options{defaultTargetName: opts}, in.progress, contextPathHash, in.builder, in.metadataFile) + _, rr, err := buildTargets(ctx, dockerCli, map[string]build.Options{defaultTargetName: opts}, in.progress, contextPathHash, in.builder, in.metadataFile, true) return rr, err }, io.NopCloser(os.Stdin), nopCloser{os.Stdout}, nopCloser{os.Stderr}) if err != nil { @@ -271,7 +271,7 @@ type nopCloser struct { func (c nopCloser) Close() error { return nil } -func buildTargets(ctx context.Context, dockerCli command.Cli, opts map[string]build.Options, progressMode, contextPathHash, instance string, metadataFile string) (imageID string, res *build.ResultContext, err error) { +func buildTargets(ctx context.Context, dockerCli command.Cli, opts map[string]build.Options, progressMode, contextPathHash, instance string, metadataFile string, allowNoOutput bool) (imageID string, res *build.ResultContext, err error) { dis, err := getInstanceOrDefault(ctx, dockerCli, instance, contextPathHash) if err != nil { return "", nil, err @@ -290,7 +290,7 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, opts map[string]bu if res == nil || driverIndex < idx { idx, res = driverIndex, gotRes } - }) + }, allowNoOutput) err1 := printer.Wait() if err == nil { err = err1 diff --git a/monitor/monitor.go b/monitor/monitor.go index 4abaa97e..43688ed6 100644 --- a/monitor/monitor.go +++ b/monitor/monitor.go @@ -13,6 +13,14 @@ import ( "golang.org/x/term" ) +const helpMessage = ` +Available commads are: + reload reloads the context and build it. + rollback re-runs the interactive container with initial rootfs contents. + exit exits monitor. + help shows this message. +` + // RunMonitor provides an interactive session for running and managing containers via specified IO. func RunMonitor(ctx context.Context, containerConfig build.ContainerConfig, reloadFunc func(context.Context) (*build.ResultContext, error), stdin io.ReadCloser, stdout, stderr io.WriteCloser) error { monitorIn, monitorOut := ioSetPipe() @@ -34,11 +42,18 @@ func RunMonitor(ctx context.Context, containerConfig build.ContainerConfig, relo m := &monitor{ invokeIO: newIOForwarder(containerIn), - muxIO: newMuxIO(ioSetIn{stdin, stdout, stderr}, []ioSetOutContext{monitorOutCtx, containerOutCtx}, 1, "Switched IO\n"), + muxIO: newMuxIO(ioSetIn{stdin, stdout, stderr}, []ioSetOutContext{monitorOutCtx, containerOutCtx}, 1, func(prev int, res int) string { + if prev == 0 && res == 0 { + // No toggle happened because container I/O isn't enabled. + return "No running interactive containers. You can start one by issuing rollback command\n" + } + return "Switched IO\n" + }), } // Start container automatically go func() { + fmt.Fprintf(stdout, "Launching interactive container. Press Ctrl-a-c to switch to monitor console\n") m.rollback(ctx, containerConfig) }() @@ -73,13 +88,18 @@ func RunMonitor(ctx context.Context, containerConfig build.ContainerConfig, relo // rollback the running container with the new result containerConfig.ResultCtx = res m.rollback(ctx, containerConfig) + fmt.Fprint(stdout, "Interactive container was restarted. Press Ctrl-a-c to switch to the new container\n") } case "rollback": m.rollback(ctx, containerConfig) + fmt.Fprint(stdout, "Interactive container was restarted. Press Ctrl-a-c to switch to the new container\n") case "exit": return + case "help": + fmt.Fprint(stdout, helpMessage) default: fmt.Printf("unknown command: %q\n", l) + fmt.Fprint(stdout, helpMessage) } } }() @@ -227,7 +247,7 @@ type ioSetOutContext struct { // newMuxIO forwards IO stream to/from "in" and "outs". // "outs" are closed automatically when "in" reaches EOF. // "in" doesn't closed automatically so the caller needs to explicitly close it. -func newMuxIO(in ioSetIn, out []ioSetOutContext, initIdx int, toggleMessage string) *muxIO { +func newMuxIO(in ioSetIn, out []ioSetOutContext, initIdx int, toggleMessage func(prev int, res int) string) *muxIO { m := &muxIO{ enabled: make(map[int]struct{}), in: in, @@ -327,7 +347,7 @@ type muxIO struct { in ioSetIn out []ioSetOutContext closedCh chan struct{} - toggleMessage string + toggleMessage func(prev int, res int) string } func (m *muxIO) waitClosed() { @@ -357,6 +377,7 @@ func (m *muxIO) toggleIO() { if m.out[m.cur].disableHook != nil { m.out[m.cur].disableHook() } + prev := m.cur for { if m.cur+1 >= m.maxCur { m.cur = 0 @@ -368,10 +389,11 @@ func (m *muxIO) toggleIO() { } break } + res := m.cur if m.out[m.cur].enableHook != nil { m.out[m.cur].enableHook() } - fmt.Fprintf(m.in.stdout, m.toggleMessage) + fmt.Fprint(m.in.stdout, m.toggleMessage(prev, res)) } func traceReader(r io.ReadCloser, f func(rune) (bool, error)) io.ReadCloser { diff --git a/monitor/monitor_test.go b/monitor/monitor_test.go index 9eb7a062..7a5226e8 100644 --- a/monitor/monitor_test.go +++ b/monitor/monitor_test.go @@ -131,7 +131,7 @@ func TestMuxIO(t *testing.T) { outBufs = append(outBufs, outBuf) outs = append(outs, ioSetOutContext{out, nil, nil}) } - mio := newMuxIO(in, outs, tt.initIdx, "") + mio := newMuxIO(in, outs, tt.initIdx, func(prev int, res int) string { return "" }) for _, i := range tt.inputs { // Add input to muxIO istr, writeback := i(mio)