diff --git a/client/stop_container.go b/client/stop_container.go index 5dd3227..9be0563 100644 --- a/client/stop_container.go +++ b/client/stop_container.go @@ -20,11 +20,34 @@ import ( cpb "github.com/openconfig/gnoi/containerz" ) -// StopContainer stops the requested instance. Stop can also force termination. -func (c *Client) StopContainer(ctx context.Context, instance string, force bool) error { +// stopOptions contains the optional parameters of a stop container call. +type stopOptions struct { + restart bool +} + +// StopOption is an option passed to a stop container call. +type StopOption func(*stopOptions) + +// WithRestart requests that the target starts the container again immediately +// after stopping it. +func WithRestart() StopOption { + return func(opt *stopOptions) { + opt.restart = true + } +} + +// StopContainer stops the requested instance. Stop can also force termination +// and restart the container after it has stopped. +func (c *Client) StopContainer(ctx context.Context, instance string, force bool, opts ...StopOption) error { + optionz := &stopOptions{} + for _, opt := range opts { + opt(optionz) + } + if _, err := c.cli.StopContainer(ctx, &cpb.StopContainerRequest{ InstanceName: instance, Force: force, + Restart: optionz.restart, }); err != nil { return err } diff --git a/client/stop_container_test.go b/client/stop_container_test.go index 602d1f5..a680b32 100644 --- a/client/stop_container_test.go +++ b/client/stop_container_test.go @@ -40,6 +40,7 @@ func TestStop(t *testing.T) { name string inInstance string inForce bool + inOpts []StopOption wantMsg *cpb.StopContainerRequest }{ @@ -52,6 +53,33 @@ func TestStop(t *testing.T) { Force: true, }, }, + { + name: "no-force", + inInstance: "some-instance", + wantMsg: &cpb.StopContainerRequest{ + InstanceName: "some-instance", + }, + }, + { + name: "restart", + inInstance: "some-instance", + inOpts: []StopOption{WithRestart()}, + wantMsg: &cpb.StopContainerRequest{ + InstanceName: "some-instance", + Restart: true, + }, + }, + { + name: "force-and-restart", + inInstance: "some-instance", + inForce: true, + inOpts: []StopOption{WithRestart()}, + wantMsg: &cpb.StopContainerRequest{ + InstanceName: "some-instance", + Force: true, + Restart: true, + }, + }, } ctx := context.Background() @@ -65,7 +93,7 @@ func TestStop(t *testing.T) { t.Fatalf("NewClient(%v) returned an unexpected error: %v", addr, err) } - if err := cli.StopContainer(ctx, tc.inInstance, tc.inForce); err != nil { + if err := cli.StopContainer(ctx, tc.inInstance, tc.inForce, tc.inOpts...); err != nil { t.Fatalf("Stop(%q, %t) returned an unexpected error: %v", tc.inInstance, tc.inForce, err) } diff --git a/cmd/container_stop.go b/cmd/container_stop.go index aa5db32..bd145e7 100644 --- a/cmd/container_stop.go +++ b/cmd/container_stop.go @@ -17,11 +17,13 @@ package cmd import ( "fmt" + "github.com/openconfig/containerz/client" "github.com/spf13/cobra" ) var ( - force bool + force bool + restart bool ) var cntStopCmd = &cobra.Command{ @@ -32,10 +34,19 @@ var cntStopCmd = &cobra.Command{ fmt.Println("--instance must be provided") } - if err := containerzClient.StopContainer(command.Context(), instance, force); err != nil { + var opts []client.StopOption + if restart { + opts = append(opts, client.WithRestart()) + } + + if err := containerzClient.StopContainer(command.Context(), instance, force, opts...); err != nil { return err } + if restart { + fmt.Printf("Successfully restarted %s\n", instance) + return nil + } fmt.Printf("Successfully stopped %s\n", instance) return nil }, @@ -46,4 +57,5 @@ func init() { cntStopCmd.PersistentFlags().StringVar(&instance, "instance", "", "Container instance to stop.") cntStopCmd.PersistentFlags().BoolVar(&force, "force", false, "Forcefully stop the container.") + cntStopCmd.PersistentFlags().BoolVar(&restart, "restart", false, "Start the container again after stopping it.") } diff --git a/containers/docker/container_stop.go b/containers/docker/container_stop.go index c4c3083..08d01e3 100644 --- a/containers/docker/container_stop.go +++ b/containers/docker/container_stop.go @@ -21,6 +21,8 @@ const maximumStopTimeout = 10 // If the Force option is set but no timeout is provided the container's StopTimeout // value is used, if set, otherwise the engine default. // If the Force option is not set, no forceful termination is performed. +// If the Restart option is set, the container is started again after it has +// stopped, using the same termination semantics. func (m *Manager) ContainerStop(ctx context.Context, instance string, opts ...options.Option) error { optionz := options.ApplyOptions(opts...) @@ -57,6 +59,18 @@ func (m *Manager) ContainerStop(ctx context.Context, instance string, opts ...op pDuration = nil } + if optionz.Restart { + // ContainerRestart stops the container using the same timeout semantics + // as ContainerStop and then starts it again. The container keeps its ID, + // configuration and restart policy. + if err := m.client.ContainerRestart(ctx, instance, container.StopOptions{Timeout: pDuration}); err != nil { + klog.Warningf("container %s failed to restart", instance) + return status.Errorf(codes.Unknown, "failed to restart container %s with error %s", + instance, err) + } + return nil + } + if err := m.client.ContainerStop(ctx, instance, container.StopOptions{Timeout: pDuration}); err != nil { klog.Warningf("container %s failed to stop", instance) return status.Errorf(codes.Unknown, "failed to stop container %s with error %s", diff --git a/containers/docker/container_stop_test.go b/containers/docker/container_stop_test.go index e2f90ca..9b3417f 100644 --- a/containers/docker/container_stop_test.go +++ b/containers/docker/container_stop_test.go @@ -18,9 +18,10 @@ type fakeStoppingDocker struct { fakeDocker cnts []types.Container - Instance string - Duration int - RemoveInstance string + Instance string + Duration int + RemoveInstance string + RestartInstance string } func (f *fakeStoppingDocker) ContainerStop(ctx context.Context, container string, options container.StopOptions) error { @@ -32,6 +33,15 @@ func (f *fakeStoppingDocker) ContainerStop(ctx context.Context, container string return nil } +func (f *fakeStoppingDocker) ContainerRestart(ctx context.Context, container string, options container.StopOptions) error { + f.RestartInstance = container + + if options.Timeout != nil { + f.Duration = *options.Timeout + } + return nil +} + func (f fakeStoppingDocker) ContainerList(ctx context.Context, options container.ListOptions) ([]types.Container, error) { return f.cnts, nil } @@ -97,6 +107,54 @@ func TestContainerStop(t *testing.T) { Duration: maximumStopTimeout, }, }, + { + name: "restart-no-force", + inInstance: "restart-no-force", + inOpts: []options.Option{options.Restart()}, + inCnts: []types.Container{ + types.Container{ + Names: []string{"restart-no-force"}, + }, + }, + wantState: &fakeStoppingDocker{ + RestartInstance: "restart-no-force", + Duration: -1, + }, + }, + { + name: "restart-with-force-no-duration", + inInstance: "restart-with-force-no-duration", + inOpts: []options.Option{options.Force(), options.Restart()}, + inCnts: []types.Container{ + types.Container{ + Names: []string{"restart-with-force-no-duration"}, + }, + }, + wantState: &fakeStoppingDocker{ + RestartInstance: "restart-with-force-no-duration", + }, + }, + { + name: "restart-with-force-and-duration", + inInstance: "restart-with-force-and-duration", + inTimeout: 1 * time.Minute, + inOpts: []options.Option{options.Force(), options.Restart()}, + inCnts: []types.Container{ + types.Container{ + Names: []string{"restart-with-force-and-duration"}, + }, + }, + wantState: &fakeStoppingDocker{ + RestartInstance: "restart-with-force-and-duration", + Duration: maximumStopTimeout, + }, + }, + { + name: "restart-no-such-instance", + inInstance: "restart-no-such-instance", + inOpts: []options.Option{options.Restart()}, + wantErr: status.Errorf(codes.NotFound, "container restart-no-such-instance was not found"), + }, } for _, tc := range tests { diff --git a/containers/docker/manager.go b/containers/docker/manager.go index 1267152..f7c151b 100644 --- a/containers/docker/manager.go +++ b/containers/docker/manager.go @@ -25,6 +25,7 @@ type docker interface { ContainerList(ctx context.Context, options container.ListOptions) ([]types.Container, error) ContainerLogs(ctx context.Context, container string, options container.LogsOptions) (io.ReadCloser, error) ContainerRemove(ctx context.Context, container string, options container.RemoveOptions) error + ContainerRestart(ctx context.Context, container string, options container.StopOptions) error ContainerStart(ctx context.Context, container string, options container.StartOptions) error ContainerStop(ctx context.Context, container string, options container.StopOptions) error ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) diff --git a/containers/docker/manager_test.go b/containers/docker/manager_test.go index 388c666..2faaf9c 100644 --- a/containers/docker/manager_test.go +++ b/containers/docker/manager_test.go @@ -49,6 +49,10 @@ func (fakeDocker) ContainerRemove(ctx context.Context, container string, options return fmt.Errorf("not implemented") } +func (fakeDocker) ContainerRestart(ctx context.Context, container string, _ container.StopOptions) error { + return fmt.Errorf("not implemented") +} + func (fakeDocker) ContainerStart(ctx context.Context, container string, options container.StartOptions) error { return fmt.Errorf("not implemented") } diff --git a/containers/options.go b/containers/options.go index 8ad2b19..4d70105 100644 --- a/containers/options.go +++ b/containers/options.go @@ -90,6 +90,9 @@ type options struct { // Force indicates that the container implementation should attempt to force the operation. Force bool + // Restart indicates that a stopped container should be started again immediately. + Restart bool + // InstanceName is the name of a running container instance. InstanceName string @@ -207,6 +210,15 @@ func Force() Option { } } +// Restart requests that the container is started again immediately after it +// has been stopped. +// Supported by: ContainerStop +func Restart() Option { + return func(p *options) { + p.Restart = true + } +} + // WithInstanceName sets the name of the instance of a container. // Supported by: ContainerStart func WithInstanceName(instance string) Option { diff --git a/containers/options_test.go b/containers/options_test.go index 4524b68..99c968a 100644 --- a/containers/options_test.go +++ b/containers/options_test.go @@ -105,6 +105,16 @@ func TestForce(t *testing.T) { } } +func TestRestart(t *testing.T) { + p := &options{} + + Restart()(p) + + if !p.Restart { + t.Errorf("Restart() did not set the restart flag") + } +} + func TestFollow(t *testing.T) { p := &options{} diff --git a/server/deploy_test.go b/server/deploy_test.go index 92d65a1..2a92c19 100644 --- a/server/deploy_test.go +++ b/server/deploy_test.go @@ -45,6 +45,7 @@ type fakeContainerManager struct { Ports map[uint32]uint32 Envs map[string]string Force bool + Restart bool Follow bool All bool Async bool @@ -133,6 +134,7 @@ func (f *fakeContainerManager) ContainerStop(_ context.Context, instance string, optionz := options.ApplyOptions(opts...) f.Instance = instance f.Force = optionz.Force + f.Restart = optionz.Restart return nil } diff --git a/server/stop_container.go b/server/stop_container.go index 715ee87..62d1cb5 100644 --- a/server/stop_container.go +++ b/server/stop_container.go @@ -23,7 +23,7 @@ import ( // StopContainer stops a container. If the container does not exist or is not running // this operation returns an error. This operation can, optionally, force -// (i.e. kill) a container. +// (i.e. kill) a container and restart it immediately after it has stopped. func (s *Server) StopContainer(ctx context.Context, request *cpb.StopContainerRequest) (*cpb.StopContainerResponse, error) { // TODO (alshabib): Consider adding a timeout to the request or use a containerz default.s opts := []options.Option{} @@ -32,6 +32,10 @@ func (s *Server) StopContainer(ctx context.Context, request *cpb.StopContainerRe opts = append(opts, options.Force()) } + if request.GetRestart() { + opts = append(opts, options.Restart()) + } + if err := s.mgr.ContainerStop(ctx, request.GetInstanceName(), opts...); err != nil { return nil, err } diff --git a/server/stop_container_test.go b/server/stop_container_test.go index 53681a5..e63ed68 100644 --- a/server/stop_container_test.go +++ b/server/stop_container_test.go @@ -55,6 +55,32 @@ func TestStop(t *testing.T) { Force: true, }, }, + { + name: "restart", + inReq: &cpb.StopContainerRequest{ + InstanceName: "some-name", + Restart: true, + }, + wantResp: &cpb.StopContainerResponse{}, + wantState: &fakeContainerManager{ + Instance: "some-name", + Restart: true, + }, + }, + { + name: "force-and-restart", + inReq: &cpb.StopContainerRequest{ + InstanceName: "some-name", + Force: true, + Restart: true, + }, + wantResp: &cpb.StopContainerResponse{}, + wantState: &fakeContainerManager{ + Instance: "some-name", + Force: true, + Restart: true, + }, + }, } for _, tc := range tests {