From 790712fa92033c05592f6321489f64979bfb2269 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:43:48 +0100 Subject: [PATCH 1/2] update tty and plain text writers to support dry run mode Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- cmd/compose/compose.go | 7 ++++++- pkg/api/api.go | 2 +- pkg/api/dryrunclient.go | 2 ++ pkg/api/proxy.go | 6 +++--- pkg/compose/compose.go | 8 ++++---- pkg/mocks/mock_docker_compose_api.go | 13 +++++++------ pkg/progress/plain.go | 17 +++++++++++++---- pkg/progress/tty.go | 20 +++++++++++++++----- pkg/progress/tty_test.go | 12 ++++++------ pkg/progress/writer.go | 26 +++++++++++++++++++------- 10 files changed, 76 insertions(+), 37 deletions(-) diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 96fda8488..ae8bb9b60 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -338,7 +338,12 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no if parallel > 0 { backend.MaxConcurrency(parallel) } - return backend.DryRunMode(dryRun) + ctx, err := backend.DryRunMode(cmd.Context(), dryRun) + if err != nil { + return err + } + cmd.SetContext(ctx) + return nil }, } diff --git a/pkg/api/api.go b/pkg/api/api.go index 892fbe5bf..a8804a6af 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -78,7 +78,7 @@ type Service interface { // MaxConcurrency defines upper limit for concurrent operations against engine API MaxConcurrency(parallel int) // DryRunMode defines if dry run applies to the command - DryRunMode(dryRun bool) error + DryRunMode(ctx context.Context, dryRun bool) (context.Context, error) // Watch services' development context and sync/notify/rebuild/restart on changes Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error } diff --git a/pkg/api/dryrunclient.go b/pkg/api/dryrunclient.go index a7fcc6b00..e2e4acaee 100644 --- a/pkg/api/dryrunclient.go +++ b/pkg/api/dryrunclient.go @@ -37,6 +37,8 @@ import ( var _ client.APIClient = &DryRunClient{} +type DryRunKey struct{} + // DryRunClient implements APIClient by delegating to implementation functions. This allows lazy init and per-method overrides type DryRunClient struct { apiClient client.APIClient diff --git a/pkg/api/proxy.go b/pkg/api/proxy.go index efcf79bbc..dfb90bde9 100644 --- a/pkg/api/proxy.go +++ b/pkg/api/proxy.go @@ -52,7 +52,7 @@ type ServiceProxy struct { ImagesFn func(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error) WatchFn func(ctx context.Context, project *types.Project, services []string, options WatchOptions) error MaxConcurrencyFn func(parallel int) - DryRunModeFn func(dryRun bool) error + DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error) interceptors []Interceptor } @@ -327,6 +327,6 @@ func (s *ServiceProxy) MaxConcurrency(i int) { s.MaxConcurrencyFn(i) } -func (s *ServiceProxy) DryRunMode(dryRun bool) error { - return s.DryRunModeFn(dryRun) +func (s *ServiceProxy) DryRunMode(ctx context.Context, dryRun bool) (context.Context, error) { + return s.DryRunModeFn(ctx, dryRun) } diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 108fb9292..7db436c18 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -66,22 +66,22 @@ func (s *composeService) MaxConcurrency(i int) { s.maxConcurrency = i } -func (s *composeService) DryRunMode(dryRun bool) error { +func (s *composeService) DryRunMode(ctx context.Context, dryRun bool) (context.Context, error) { if dryRun { cli, err := command.NewDockerCli() if err != nil { - return err + return ctx, err } err = cli.Initialize(flags.NewClientOptions(), command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) { dryRunClient := api.NewDryRunClient(s.apiClient()) return dryRunClient, nil })) if err != nil { - return err + return ctx, err } s.dockerCli = cli } - return nil + return context.WithValue(ctx, api.DryRunKey{}, dryRun), nil } func (s *composeService) stdout() *streams.Out { diff --git a/pkg/mocks/mock_docker_compose_api.go b/pkg/mocks/mock_docker_compose_api.go index d98eda846..9cfcf97ba 100644 --- a/pkg/mocks/mock_docker_compose_api.go +++ b/pkg/mocks/mock_docker_compose_api.go @@ -108,17 +108,18 @@ func (mr *MockServiceMockRecorder) Down(ctx, projectName, options interface{}) * } // DryRunMode mocks base method. -func (m *MockService) DryRunMode(dryRun bool) error { +func (m *MockService) DryRunMode(ctx context.Context, dryRun bool) (context.Context, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DryRunMode", dryRun) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "DryRunMode", ctx, dryRun) + ret0, _ := ret[0].(context.Context) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DryRunMode indicates an expected call of DryRunMode. -func (mr *MockServiceMockRecorder) DryRunMode(dryRun interface{}) *gomock.Call { +func (mr *MockServiceMockRecorder) DryRunMode(ctx, dryRun interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DryRunMode", reflect.TypeOf((*MockService)(nil).DryRunMode), dryRun) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DryRunMode", reflect.TypeOf((*MockService)(nil).DryRunMode), ctx, dryRun) } // Events mocks base method. diff --git a/pkg/progress/plain.go b/pkg/progress/plain.go index 3524074da..ce20a01f9 100644 --- a/pkg/progress/plain.go +++ b/pkg/progress/plain.go @@ -23,8 +23,9 @@ import ( ) type plainWriter struct { - out io.Writer - done chan bool + out io.Writer + done chan bool + dryRun bool } func (p *plainWriter) Start(ctx context.Context) error { @@ -37,7 +38,11 @@ func (p *plainWriter) Start(ctx context.Context) error { } func (p *plainWriter) Event(e Event) { - fmt.Fprintln(p.out, e.ID, e.Text, e.StatusText) + prefix := "" + if p.dryRun { + prefix = "DRY RUN MODE - " + } + fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.StatusText) } func (p *plainWriter) Events(events []Event) { @@ -47,7 +52,11 @@ func (p *plainWriter) Events(events []Event) { } func (p *plainWriter) TailMsgf(m string, args ...interface{}) { - fmt.Fprintln(p.out, append([]interface{}{m}, args...)...) + prefix := "" + if p.dryRun { + prefix = DRYRUN_PREFIX + } + fmt.Fprintln(p.out, append([]interface{}{prefix, m}, args...)...) } func (p *plainWriter) Stop() { diff --git a/pkg/progress/tty.go b/pkg/progress/tty.go index a0792cd99..9352ac0a7 100644 --- a/pkg/progress/tty.go +++ b/pkg/progress/tty.go @@ -40,6 +40,7 @@ type ttyWriter struct { done chan bool mtx *sync.Mutex tailEvents []string + dryRun bool } func (w *ttyWriter) Start(ctx context.Context) error { @@ -107,7 +108,11 @@ func (w *ttyWriter) Events(events []Event) { func (w *ttyWriter) TailMsgf(msg string, args ...interface{}) { w.mtx.Lock() defer w.mtx.Unlock() - w.tailEvents = append(w.tailEvents, fmt.Sprintf(msg, args...)) + msgWithPrefix := msg + if w.dryRun { + msgWithPrefix = strings.TrimSpace(DRYRUN_PREFIX + msg) + } + w.tailEvents = append(w.tailEvents, fmt.Sprintf(msgWithPrefix, args...)) } func (w *ttyWriter) printTailEvents() { @@ -167,7 +172,7 @@ func (w *ttyWriter) print() { //nolint:gocyclo if event.ParentID != "" { continue } - line := lineText(event, "", terminalWidth, statusPadding, runtime.GOOS != "windows") + line := lineText(event, "", terminalWidth, statusPadding, runtime.GOOS != "windows", w.dryRun) fmt.Fprint(w.out, line) numLines++ for _, v := range w.eventIDs { @@ -176,7 +181,7 @@ func (w *ttyWriter) print() { //nolint:gocyclo if skipChildEvents { continue } - line := lineText(ev, " ", terminalWidth, statusPadding, runtime.GOOS != "windows") + line := lineText(ev, " ", terminalWidth, statusPadding, runtime.GOOS != "windows", w.dryRun) fmt.Fprint(w.out, line) numLines++ } @@ -191,7 +196,7 @@ func (w *ttyWriter) print() { //nolint:gocyclo w.numLines = numLines } -func lineText(event Event, pad string, terminalWidth, statusPadding int, color bool) string { +func lineText(event Event, pad string, terminalWidth, statusPadding int, color bool, dryRun bool) string { endTime := time.Now() if event.Status != Working { endTime = event.startTime @@ -199,6 +204,10 @@ func lineText(event Event, pad string, terminalWidth, statusPadding int, color b endTime = event.endTime } } + prefix := "" + if dryRun { + prefix = DRYRUN_PREFIX + } elapsed := endTime.Sub(event.startTime).Seconds() @@ -215,9 +224,10 @@ func lineText(event Event, pad string, terminalWidth, statusPadding int, color b if maxStatusLen > 0 && len(status) > maxStatusLen { status = status[:maxStatusLen] + "..." } - text := fmt.Sprintf("%s %s %s %s%s %s", + text := fmt.Sprintf("%s %s%s %s %s%s %s", pad, event.spinner.String(), + prefix, event.ID, event.Text, strings.Repeat(" ", padding), diff --git a/pkg/progress/tty_test.go b/pkg/progress/tty_test.go index e9e0c7e2d..53f9d2233 100644 --- a/pkg/progress/tty_test.go +++ b/pkg/progress/tty_test.go @@ -41,22 +41,22 @@ func TestLineText(t *testing.T) { lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text)) - out := lineText(ev, "", 50, lineWidth, true) + out := lineText(ev, "", 50, lineWidth, true, false) assert.Equal(t, out, "\x1b[37m . id Text Status 0.0s\n\x1b[0m") - out = lineText(ev, "", 50, lineWidth, false) + out = lineText(ev, "", 50, lineWidth, false, false) assert.Equal(t, out, " . id Text Status 0.0s\n") ev.Status = Done - out = lineText(ev, "", 50, lineWidth, true) + out = lineText(ev, "", 50, lineWidth, true, false) assert.Equal(t, out, "\x1b[34m . id Text Status 0.0s\n\x1b[0m") ev.Status = Error - out = lineText(ev, "", 50, lineWidth, true) + out = lineText(ev, "", 50, lineWidth, true, false) assert.Equal(t, out, "\x1b[31m . id Text Status 0.0s\n\x1b[0m") ev.Status = Warning - out = lineText(ev, "", 50, lineWidth, true) + out = lineText(ev, "", 50, lineWidth, true, false) assert.Equal(t, out, "\x1b[33m . id Text Status 0.0s\n\x1b[0m") } @@ -75,7 +75,7 @@ func TestLineTextSingleEvent(t *testing.T) { lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text)) - out := lineText(ev, "", 50, lineWidth, true) + out := lineText(ev, "", 50, lineWidth, true, false) assert.Equal(t, out, "\x1b[34m . id Text Status 0.0s\n\x1b[0m") } diff --git a/pkg/progress/writer.go b/pkg/progress/writer.go index 2914364e4..8858c0734 100644 --- a/pkg/progress/writer.go +++ b/pkg/progress/writer.go @@ -21,11 +21,17 @@ import ( "os" "sync" + "github.com/docker/compose/v2/pkg/api" + "github.com/containerd/console" "github.com/moby/term" "golang.org/x/sync/errgroup" ) +const ( + DRYRUN_PREFIX = " DRY-RUN MODE - " +) + // Writer can write multiple progress events type Writer interface { Start(context.Context) error @@ -66,7 +72,7 @@ func Run(ctx context.Context, pf progressFunc) error { // RunWithStatus will run a writer and the progress function in parallel and return a status func RunWithStatus(ctx context.Context, pf progressFuncWithStatus) (string, error) { eg, _ := errgroup.WithContext(ctx) - w, err := NewWriter(os.Stderr) + w, err := NewWriter(ctx, os.Stderr) var result string if err != nil { return "", err @@ -103,21 +109,26 @@ const ( var Mode = ModeAuto // NewWriter returns a new multi-progress writer -func NewWriter(out console.File) (Writer, error) { +func NewWriter(ctx context.Context, out console.File) (Writer, error) { _, isTerminal := term.GetFdInfo(out) + dryRun, ok := ctx.Value(api.DryRunKey{}).(bool) + if !ok { + dryRun = false + } if Mode == ModeAuto && isTerminal { - return newTTYWriter(out) + return newTTYWriter(out, dryRun) } if Mode == ModeTTY { - return newTTYWriter(out) + return newTTYWriter(out, dryRun) } return &plainWriter{ - out: out, - done: make(chan bool), + out: out, + done: make(chan bool), + dryRun: dryRun, }, nil } -func newTTYWriter(out console.File) (Writer, error) { +func newTTYWriter(out console.File, dryRun bool) (Writer, error) { con, err := console.ConsoleFromFile(out) if err != nil { return nil, err @@ -130,5 +141,6 @@ func newTTYWriter(out console.File) (Writer, error) { repeated: false, done: make(chan bool), mtx: &sync.Mutex{}, + dryRun: dryRun, }, nil } From 982a8ccb88de4a0ed3496b9c1e91ec05c6cbc3d9 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:44:09 +0100 Subject: [PATCH 2/2] support dry-run for kill command Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- pkg/api/dryrunclient.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/dryrunclient.go b/pkg/api/dryrunclient.go index e2e4acaee..0d9d1e96d 100644 --- a/pkg/api/dryrunclient.go +++ b/pkg/api/dryrunclient.go @@ -63,7 +63,7 @@ func (d *DryRunClient) ContainerCreate(ctx context.Context, config *containerTyp } func (d *DryRunClient) ContainerKill(ctx context.Context, container, signal string) error { - return ErrNotImplemented + return nil } func (d *DryRunClient) ContainerPause(ctx context.Context, container string) error {