diff --git a/aci/compose.go b/aci/compose.go index 65343670..7f17f961 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -64,6 +64,10 @@ func (cs *aciComposeService) Start(ctx context.Context, project *types.Project, return errdefs.ErrNotImplemented } +func (cs *aciComposeService) Restart(ctx context.Context, project *types.Project, options compose.RestartOptions) error { + return errdefs.ErrNotImplemented +} + func (cs *aciComposeService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented } diff --git a/api/client/compose.go b/api/client/compose.go index c5b1dca3..c59be7e1 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -48,6 +48,10 @@ func (c *composeService) Start(ctx context.Context, project *types.Project, opti return errdefs.ErrNotImplemented } +func (c *composeService) Restart(ctx context.Context, project *types.Project, options compose.RestartOptions) error { + return errdefs.ErrNotImplemented +} + func (c *composeService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented } diff --git a/api/compose/api.go b/api/compose/api.go index c6269328..de624961 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -38,6 +38,8 @@ type Service interface { Create(ctx context.Context, project *types.Project, opts CreateOptions) error // Start executes the equivalent to a `compose start` Start(ctx context.Context, project *types.Project, options StartOptions) error + // Restart restarts containers + Restart(ctx context.Context, project *types.Project, options RestartOptions) error // Stop executes the equivalent to a `compose stop` Stop(ctx context.Context, project *types.Project, options StopOptions) error // Up executes the equivalent to a `compose up` @@ -106,6 +108,12 @@ type StartOptions struct { Services []string } +// RestartOptions group options of the Restart API +type RestartOptions struct { + // Timeout override container restart timeout + Timeout *time.Duration +} + // StopOptions group options of the Stop API type StopOptions struct { // Timeout override container stop timeout diff --git a/api/progress/event.go b/api/progress/event.go index 2898a038..979a0e53 100644 --- a/api/progress/event.go +++ b/api/progress/event.go @@ -68,6 +68,16 @@ func StartedEvent(ID string) Event { return NewEvent(ID, Done, "Started") } +// RestartingEvent creates a new Restarting in progress Event +func RestartingEvent(ID string) Event { + return NewEvent(ID, Working, "Restarting") +} + +// RestartedEvent creates a new Restarted in progress Event +func RestartedEvent(ID string) Event { + return NewEvent(ID, Done, "Restarted") +} + // RunningEvent creates a new Running in progress Event func RunningEvent(ID string) Event { return NewEvent(ID, Done, "Running") diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index c6b18039..5c3eb92c 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -127,6 +127,7 @@ func Command(contextType string) *cobra.Command { upCommand(&opts, contextType), downCommand(&opts, contextType), startCommand(&opts), + restartCommand(&opts), stopCommand(&opts), psCommand(&opts), listCommand(contextType), diff --git a/cli/cmd/compose/restart.go b/cli/cmd/compose/restart.go new file mode 100644 index 00000000..4d10568b --- /dev/null +++ b/cli/cmd/compose/restart.go @@ -0,0 +1,70 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "context" + "time" + + "github.com/spf13/cobra" + + "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" +) + +type restartOptions struct { + *projectOptions + timeout int +} + +func restartCommand(p *projectOptions) *cobra.Command { + opts := restartOptions{ + projectOptions: p, + } + restartCmd := &cobra.Command{ + Use: "restart", + Short: "Restart containers", + RunE: func(cmd *cobra.Command, args []string) error { + return runRestart(cmd.Context(), opts, args) + }, + } + flags := restartCmd.Flags() + flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds") + + return restartCmd +} + +func runRestart(ctx context.Context, opts restartOptions, services []string) error { + c, err := client.New(ctx) + if err != nil { + return err + } + + project, err := opts.toProject(services) + if err != nil { + return err + } + + timeout := time.Duration(opts.timeout) * time.Second + _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { + return "", c.ComposeService().Restart(ctx, project, compose.RestartOptions{ + Timeout: &timeout, + }) + }) + return err +} diff --git a/docs/reference/compose_restart.md b/docs/reference/compose_restart.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/reference/docker_compose.yaml b/docs/reference/docker_compose.yaml index cfff7605..1910fdb7 100644 --- a/docs/reference/docker_compose.yaml +++ b/docs/reference/docker_compose.yaml @@ -62,6 +62,7 @@ cname: - docker compose ps - docker compose pull - docker compose push + - docker compose restart - docker compose rm - docker compose run - docker compose start @@ -83,6 +84,7 @@ clink: - docker_compose_ps.yaml - docker_compose_pull.yaml - docker_compose_push.yaml + - docker_compose_restart.yaml - docker_compose_rm.yaml - docker_compose_run.yaml - docker_compose_start.yaml diff --git a/docs/reference/docker_compose_restart.yaml b/docs/reference/docker_compose_restart.yaml new file mode 100644 index 00000000..0bdce775 --- /dev/null +++ b/docs/reference/docker_compose_restart.yaml @@ -0,0 +1,23 @@ +command: docker compose restart +short: Restart containers +long: Restart containers +usage: docker compose restart +pname: docker compose +plink: docker_compose.yaml +options: + - option: timeout + shorthand: t + value_type: int + default_value: "10" + description: Specify a shutdown timeout in seconds + deprecated: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false +deprecated: false +experimental: false +experimentalcli: false +kubernetes: false +swarm: false + diff --git a/ecs/local/compose.go b/ecs/local/compose.go index 69958af9..fc76f393 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -57,6 +57,10 @@ func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, o return e.compose.Start(ctx, project, options) } +func (e ecsLocalSimulation) Restart(ctx context.Context, project *types.Project, options compose.RestartOptions) error { + return e.compose.Restart(ctx, project, options) +} + func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return e.compose.Stop(ctx, project, options) } diff --git a/ecs/up.go b/ecs/up.go index aa937f51..fa334089 100644 --- a/ecs/up.go +++ b/ecs/up.go @@ -51,6 +51,10 @@ func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, optio return errdefs.ErrNotImplemented } +func (b *ecsAPIService) Restart(ctx context.Context, project *types.Project, options compose.RestartOptions) error { + return errdefs.ErrNotImplemented +} + func (b *ecsAPIService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented } diff --git a/kube/compose.go b/kube/compose.go index adf4992f..78412634 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -188,6 +188,11 @@ func (s *composeService) Start(ctx context.Context, project *types.Project, opti return errdefs.ErrNotImplemented } +// Restart executes the equivalent to a `compose restart` +func (s *composeService) Restart(ctx context.Context, project *types.Project, options compose.RestartOptions) error { + return errdefs.ErrNotImplemented +} + // Stop executes the equivalent to a `compose stop` func (s *composeService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented diff --git a/local/compose/convergence.go b/local/compose/convergence.go index bcd09b0a..f3b03dda 100644 --- a/local/compose/convergence.go +++ b/local/compose/convergence.go @@ -390,3 +390,26 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec } return eg.Wait() } + +func (s *composeService) restartService(ctx context.Context, serviceName string, timeout *time.Duration) error { + containerState, err := GetContextContainerState(ctx) + if err != nil { + return err + } + containers := containerState.GetContainers().filter(isService(serviceName)) + w := progress.ContextWriter(ctx) + eg, ctx := errgroup.WithContext(ctx) + for _, c := range containers { + container := c + eg.Go(func() error { + eventName := getContainerProgressName(container) + w.Event(progress.RestartingEvent(eventName)) + err := s.apiClient.ContainerRestart(ctx, container.ID, timeout) + if err == nil { + w.Event(progress.StartedEvent(eventName)) + } + return err + }) + } + return eg.Wait() +} diff --git a/local/compose/restart.go b/local/compose/restart.go new file mode 100644 index 00000000..4a273ec6 --- /dev/null +++ b/local/compose/restart.go @@ -0,0 +1,39 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "context" + + "github.com/docker/compose-cli/api/compose" + + "github.com/compose-spec/compose-go/types" +) + +func (s *composeService) Restart(ctx context.Context, project *types.Project, options compose.RestartOptions) error { + ctx, err := s.getUpdatedContainersStateContext(ctx, project.Name) + if err != nil { + return err + } + err = InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { + return s.restartService(ctx, service.Name, options.Timeout) + }) + if err != nil { + return err + } + return nil +} diff --git a/local/compose/status.go b/local/compose/status.go index 854b1918..c75eb333 100644 --- a/local/compose/status.go +++ b/local/compose/status.go @@ -20,6 +20,7 @@ import ( "context" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/pkg/errors" ) @@ -100,3 +101,17 @@ func GetContextContainerState(ctx context.Context) (ContainersState, error) { } return cState, nil } + +func (s composeService) getUpdatedContainersStateContext(ctx context.Context, projectName string) (context.Context, error) { + observedState, err := s.apiClient.ContainerList(ctx, types.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(projectName), + ), + All: true, + }) + if err != nil { + return nil, err + } + containerState := NewContainersState(observedState) + return context.WithValue(ctx, ContainersKey{}, containerState), nil +} diff --git a/local/e2e/compose/fixtures/restart-test/compose.yml b/local/e2e/compose/fixtures/restart-test/compose.yml new file mode 100644 index 00000000..dd87ec20 --- /dev/null +++ b/local/e2e/compose/fixtures/restart-test/compose.yml @@ -0,0 +1,4 @@ +services: + restart: + image: busybox + command: ash -c "if [[ -f /tmp/restart.lock ]] ; then sleep infinity; else touch /tmp/restart.lock; fi" diff --git a/local/e2e/compose/restart_test.go b/local/e2e/compose/restart_test.go new file mode 100644 index 00000000..3bc30eef --- /dev/null +++ b/local/e2e/compose/restart_test.go @@ -0,0 +1,65 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "strings" + "testing" + "time" + + testify "github.com/stretchr/testify/assert" + "gotest.tools/v3/assert" + + . "github.com/docker/compose-cli/utils/e2e" +) + +func TestRestart(t *testing.T) { + c := NewParallelE2eCLI(t, binDir) + const projectName = "e2e-restart" + + getServiceRegx := func(service string, status string) string { + // match output with random spaces like: + // e2e-start-stop_db_1 db running + return fmt.Sprintf("%s_%s_1\\s+%s\\s+%s", projectName, service, service, status) + } + + t.Run("Up a project", func(t *testing.T) { + // This is just to ensure the containers do NOT exist + c.RunDockerOrExitError("compose", "--project-name", projectName, "down") + + res := c.RunDockerOrExitError("compose", "-f", "./fixtures/restart-test/compose.yml", "--project-name", projectName, "up", "-d") + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart_restart_1 Started"), res.Combined()) + + // Give the time for it to exit + time.Sleep(time.Second) + + res = c.RunDockerOrExitError("compose", "--project-name", projectName, "ps", "-a") + testify.Regexp(t, getServiceRegx("restart", "exited"), res.Stdout()) + + _ = c.RunDockerOrExitError("compose", "-f", "./fixtures/restart-test/compose.yml", "--project-name", projectName, "restart") + + // Give the same time but it must NOT exit + time.Sleep(time.Second) + + res = c.RunDockerOrExitError("compose", "--project-name", projectName, "ps") + testify.Regexp(t, getServiceRegx("restart", "running"), res.Stdout()) + + // Clean up + c.RunDockerOrExitError("compose", "--project-name", projectName, "down") + }) +}