diff --git a/cli/command/container/client_test.go b/cli/command/container/client_test.go index deae11a73b7f..adc39f9bedb4 100644 --- a/cli/command/container/client_test.go +++ b/cli/command/container/client_test.go @@ -35,6 +35,8 @@ type fakeClient struct { containerExportFunc func(string) (io.ReadCloser, error) containerExecResizeFunc func(id string, options container.ResizeOptions) error containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error + containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error + containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error containerKillFunc func(ctx context.Context, containerID, signal string) error containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) @@ -175,6 +177,20 @@ func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.A return container.PruneReport{}, nil } +func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error { + if f.containerRestartFunc != nil { + return f.containerRestartFunc(ctx, containerID, options) + } + return nil +} + +func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error { + if f.containerStopFunc != nil { + return f.containerStopFunc(ctx, containerID, options) + } + return nil +} + func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { if f.containerAttachFunc != nil { return f.containerAttachFunc(ctx, containerID, options) diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go index 8e7c0d4a026f..950bb81e0453 100644 --- a/cli/command/container/restart.go +++ b/cli/command/container/restart.go @@ -55,6 +55,8 @@ func runRestart(ctx context.Context, dockerCli command.Cli, opts *restartOptions if opts.timeoutChanged { timeout = &opts.timeout } + + // TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove" for _, name := range opts.containers { err := dockerCli.Client().ContainerRestart(ctx, name, container.StopOptions{ Signal: opts.signal, diff --git a/cli/command/container/restart_test.go b/cli/command/container/restart_test.go new file mode 100644 index 000000000000..8a5a69c1bcf7 --- /dev/null +++ b/cli/command/container/restart_test.go @@ -0,0 +1,85 @@ +package container + +import ( + "context" + "io" + "sort" + "sync" + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/errdefs" + "github.com/pkg/errors" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestRestart(t *testing.T) { + for _, tc := range []struct { + name string + args []string + restarted []string + expectedOpts container.StopOptions + expectedErr string + }{ + { + name: "without options", + args: []string{"container-1", "container-2"}, + restarted: []string{"container-1", "container-2"}, + }, + { + name: "with unknown container", + args: []string{"container-1", "nosuchcontainer", "container-2"}, + expectedErr: "no such container", + restarted: []string{"container-1", "container-2"}, + }, + { + name: "with -t", + args: []string{"-t", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + restarted: []string{"container-1"}, + }, + { + name: "with --time", + args: []string{"--time", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + restarted: []string{"container-1"}, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var restarted []string + mutex := new(sync.Mutex) + + cli := test.NewFakeCli(&fakeClient{ + containerRestartFunc: func(ctx context.Context, containerID string, options container.StopOptions) error { + assert.Check(t, is.DeepEqual(options, tc.expectedOpts)) + if containerID == "nosuchcontainer" { + return errdefs.NotFound(errors.New("Error: no such container: " + containerID)) + } + + // TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove" + mutex.Lock() + restarted = append(restarted, containerID) + mutex.Unlock() + return nil + }, + Version: "1.36", + }) + cmd := NewRestartCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + + err := cmd.Execute() + if tc.expectedErr != "" { + assert.Check(t, is.ErrorContains(err, tc.expectedErr)) + } else { + assert.Check(t, is.Nil(err)) + } + sort.Strings(restarted) + assert.Check(t, is.DeepEqual(restarted, tc.restarted)) + }) + } +} diff --git a/cli/command/container/stop_test.go b/cli/command/container/stop_test.go new file mode 100644 index 000000000000..44893d68abd9 --- /dev/null +++ b/cli/command/container/stop_test.go @@ -0,0 +1,86 @@ +package container + +import ( + "context" + "io" + "sort" + "sync" + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/errdefs" + "github.com/pkg/errors" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestStop(t *testing.T) { + for _, tc := range []struct { + name string + args []string + stopped []string + expectedOpts container.StopOptions + expectedErr string + }{ + { + name: "without options", + args: []string{"container-1", "container-2"}, + stopped: []string{"container-1", "container-2"}, + }, + { + name: "with unknown container", + args: []string{"container-1", "nosuchcontainer", "container-2"}, + expectedErr: "no such container", + stopped: []string{"container-1", "container-2"}, + }, + { + name: "with -t", + args: []string{"-t", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + stopped: []string{"container-1"}, + }, + { + name: "with --time", + args: []string{"--time", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + stopped: []string{"container-1"}, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var stopped []string + mutex := new(sync.Mutex) + + cli := test.NewFakeCli(&fakeClient{ + containerStopFunc: func(ctx context.Context, containerID string, options container.StopOptions) error { + assert.Check(t, is.DeepEqual(options, tc.expectedOpts)) + if containerID == "nosuchcontainer" { + return errdefs.NotFound(errors.New("Error: no such container: " + containerID)) + } + + // containerStopFunc is called in parallel for each container + // so append must be synchronized. + mutex.Lock() + stopped = append(stopped, containerID) + mutex.Unlock() + return nil + }, + Version: "1.36", + }) + cmd := NewStopCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + + err := cmd.Execute() + if tc.expectedErr != "" { + assert.Check(t, is.ErrorContains(err, tc.expectedErr)) + } else { + assert.Check(t, is.Nil(err)) + } + sort.Strings(stopped) + assert.Check(t, is.DeepEqual(stopped, tc.stopped)) + }) + } +}