diff --git a/cli/command/service/client_test.go b/cli/command/service/client_test.go index b4f8905159..a886ff6a6d 100644 --- a/cli/command/service/client_test.go +++ b/cli/command/service/client_test.go @@ -5,17 +5,18 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "golang.org/x/net/context" + // Import builders to get the builder function as package function + . "github.com/docker/cli/cli/internal/test/builders" ) type fakeClient struct { client.Client - serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error) + serviceInspectWithRawFunc func(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) + serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) + serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error) } -func (f *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { - if f.serviceListFunc != nil { - return f.serviceListFunc(ctx, options) - } +func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { return nil, nil } @@ -23,10 +24,30 @@ func (f *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions return nil, nil } -func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { +func (f *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) { + if f.serviceInspectWithRawFunc != nil { + return f.serviceInspectWithRawFunc(ctx, serviceID, options) + } + + return *Service(ServiceID(serviceID)), []byte{}, nil +} + +func (f *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { + if f.serviceListFunc != nil { + return f.serviceListFunc(ctx, options) + } + return nil, nil } +func (f *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) { + if f.serviceUpdateFunc != nil { + return f.serviceUpdateFunc(ctx, serviceID, version, service, options) + } + + return types.ServiceUpdateResponse{}, nil +} + func newService(id string, name string) swarm.Service { return swarm.Service{ ID: id, diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go index 94e3a97d4e..107abf32d0 100644 --- a/cli/command/service/cmd.go +++ b/cli/command/service/cmd.go @@ -26,6 +26,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { newScaleCommand(dockerCli), newUpdateCommand(dockerCli), newLogsCommand(dockerCli), + newRollbackCommand(dockerCli), ) return cmd } diff --git a/cli/command/service/create.go b/cli/command/service/create.go index e4b42a45ce..ecf7a37e28 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -12,7 +12,7 @@ import ( "golang.org/x/net/context" ) -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newCreateCommand(dockerCli command.Cli) *cobra.Command { opts := newServiceOptions() cmd := &cobra.Command{ @@ -62,7 +62,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions) error { +func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *serviceOptions) error { apiClient := dockerCli.Client() createOpts := types.ServiceCreateOptions{} diff --git a/cli/command/service/helpers.go b/cli/command/service/helpers.go index 8fb82c7cca..de6878c17b 100644 --- a/cli/command/service/helpers.go +++ b/cli/command/service/helpers.go @@ -15,7 +15,7 @@ import ( // waitOnService waits for the service to converge. It outputs a progress bar, // if appropriate based on the CLI flags. -func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID string, quiet bool) error { +func waitOnService(ctx context.Context, dockerCli command.Cli, serviceID string, quiet bool) error { errChan := make(chan error, 1) pipeReader, pipeWriter := io.Pipe() diff --git a/cli/command/service/inspect.go b/cli/command/service/inspect.go index ea0009d3a5..dbe8ac8ddc 100644 --- a/cli/command/service/inspect.go +++ b/cli/command/service/inspect.go @@ -20,7 +20,7 @@ type inspectOptions struct { pretty bool } -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInspectCommand(dockerCli command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -43,7 +43,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { +func runInspect(dockerCli command.Cli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/service/remove.go b/cli/command/service/remove.go index bc6a202f64..38833b2e7e 100644 --- a/cli/command/service/remove.go +++ b/cli/command/service/remove.go @@ -11,7 +11,7 @@ import ( "golang.org/x/net/context" ) -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "rm SERVICE [SERVICE...]", @@ -27,7 +27,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runRemove(dockerCli *command.DockerCli, sids []string) error { +func runRemove(dockerCli command.Cli, sids []string) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/service/rollback.go b/cli/command/service/rollback.go new file mode 100644 index 0000000000..5cef656255 --- /dev/null +++ b/cli/command/service/rollback.go @@ -0,0 +1,65 @@ +package service + +import ( + "context" + "fmt" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/docker/api/types" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func newRollbackCommand(dockerCli command.Cli) *cobra.Command { + options := newServiceOptions() + + cmd := &cobra.Command{ + Use: "rollback [OPTIONS] SERVICE", + Short: "Revert changes to a service's configuration", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runRollback(dockerCli, cmd.Flags(), options, args[0]) + }, + Tags: map[string]string{"version": "1.31"}, + } + + flags := cmd.Flags() + flags.BoolVarP(&options.quiet, flagQuiet, "q", false, "Suppress progress output") + addDetachFlag(flags, &options.detach) + + return cmd +} + +func runRollback(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error { + apiClient := dockerCli.Client() + ctx := context.Background() + + service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) + if err != nil { + return err + } + + spec := &service.Spec + updateOpts := types.ServiceUpdateOptions{ + Rollback: "previous", + } + + response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts) + if err != nil { + return err + } + + for _, warning := range response.Warnings { + fmt.Fprintln(dockerCli.Err(), warning) + } + + fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) + + if options.detach { + warnDetachDefault(dockerCli.Err(), apiClient.ClientVersion(), flags, "rolled back") + return nil + } + + return waitOnService(ctx, dockerCli, serviceID, options.quiet) +} diff --git a/cli/command/service/rollback_test.go b/cli/command/service/rollback_test.go new file mode 100644 index 0000000000..48be5076d7 --- /dev/null +++ b/cli/command/service/rollback_test.go @@ -0,0 +1,104 @@ +package service + +import ( + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" +) + +func TestRollback(t *testing.T) { + testCases := []struct { + name string + args []string + serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) + expectedDockerCliErr string + }{ + { + name: "rollback-service", + args: []string{"service-id"}, + }, + { + name: "rollback-service-with-warnings", + args: []string{"service-id"}, + serviceUpdateFunc: func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) { + response := types.ServiceUpdateResponse{} + + response.Warnings = []string{ + "- warning 1", + "- warning 2", + } + + return response, nil + }, + expectedDockerCliErr: "- warning 1\n- warning 2", + }, + } + + for _, tc := range testCases { + cli := test.NewFakeCli(&fakeClient{ + serviceUpdateFunc: tc.serviceUpdateFunc, + }) + cmd := newRollbackCommand(cli) + cmd.SetArgs(tc.args) + cmd.Flags().Set("quiet", "true") + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(cli.ErrBuffer().String()), tc.expectedDockerCliErr) + } +} + +func TestRollbackWithErrors(t *testing.T) { + testCases := []struct { + name string + args []string + serviceInspectWithRawFunc func(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) + serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires exactly 1 argument", + }, + { + name: "too-many-args", + args: []string{"service-id-1", "service-id-2"}, + expectedError: "requires exactly 1 argument", + }, + { + name: "service-does-not-exists", + args: []string{"service-id"}, + serviceInspectWithRawFunc: func(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) { + return swarm.Service{}, []byte{}, fmt.Errorf("no such services: %s", serviceID) + }, + expectedError: "no such services: service-id", + }, + { + name: "service-update-failed", + args: []string{"service-id"}, + serviceUpdateFunc: func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) { + return types.ServiceUpdateResponse{}, fmt.Errorf("no such services: %s", serviceID) + }, + expectedError: "no such services: service-id", + }, + } + + for _, tc := range testCases { + cmd := newRollbackCommand( + test.NewFakeCli(&fakeClient{ + serviceInspectWithRawFunc: tc.serviceInspectWithRawFunc, + serviceUpdateFunc: tc.serviceUpdateFunc, + })) + cmd.SetArgs(tc.args) + cmd.Flags().Set("quiet", "true") + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} diff --git a/cli/command/service/scale.go b/cli/command/service/scale.go index f58786156a..4910d4fa50 100644 --- a/cli/command/service/scale.go +++ b/cli/command/service/scale.go @@ -19,7 +19,7 @@ type scaleOptions struct { detach bool } -func newScaleCommand(dockerCli *command.DockerCli) *cobra.Command { +func newScaleCommand(dockerCli command.Cli) *cobra.Command { options := &scaleOptions{} cmd := &cobra.Command{ @@ -54,7 +54,7 @@ func scaleArgs(cmd *cobra.Command, args []string) error { return nil } -func runScale(dockerCli *command.DockerCli, flags *pflag.FlagSet, options *scaleOptions, args []string) error { +func runScale(dockerCli command.Cli, flags *pflag.FlagSet, options *scaleOptions, args []string) error { var errs []string var serviceIDs []string ctx := context.Background() @@ -96,7 +96,7 @@ func runScale(dockerCli *command.DockerCli, flags *pflag.FlagSet, options *scale return errors.Errorf(strings.Join(errs, "\n")) } -func runServiceScale(ctx context.Context, dockerCli *command.DockerCli, serviceID string, scale uint64) error { +func runServiceScale(ctx context.Context, dockerCli command.Cli, serviceID string, scale uint64) error { client := dockerCli.Client() service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) diff --git a/cli/command/service/update.go b/cli/command/service/update.go index dba05a1b1e..90057adedf 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -22,7 +22,7 @@ import ( "golang.org/x/net/context" ) -func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUpdateCommand(dockerCli command.Cli) *cobra.Command { options := newServiceOptions() cmd := &cobra.Command{ @@ -103,7 +103,7 @@ func newListOptsVar() *opts.ListOpts { } // nolint: gocyclo -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error { +func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error { apiClient := dockerCli.Client() ctx := context.Background() diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 458575c87b..6950935b82 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -875,9 +875,10 @@ x3ti0erg11rjpg64m75kej2mz-hosttempl * [service inspect](service_inspect.md) * [service logs](service_logs.md) * [service ls](service_ls.md) -* [service rm](service_rm.md) -* [service scale](service_scale.md) * [service ps](service_ps.md) +* [service rm](service_rm.md) +* [service rollback](service_rollback.md) +* [service scale](service_scale.md) * [service update](service_update.md) diff --git a/docs/reference/commandline/service_inspect.md b/docs/reference/commandline/service_inspect.md index af09b52be2..cbc954a5ae 100644 --- a/docs/reference/commandline/service_inspect.md +++ b/docs/reference/commandline/service_inspect.md @@ -164,7 +164,8 @@ $ docker service inspect --format='{{.Spec.Mode.Replicated.Replicas}}' redis * [service create](service_create.md) * [service logs](service_logs.md) * [service ls](service_ls.md) -* [service rm](service_rm.md) -* [service scale](service_scale.md) * [service ps](service_ps.md) +* [service rm](service_rm.md) +* [service rollback](service_rollback.md) +* [service scale](service_scale.md) * [service update](service_update.md) diff --git a/docs/reference/commandline/service_logs.md b/docs/reference/commandline/service_logs.md index cf12169bcf..4f8f35a62d 100644 --- a/docs/reference/commandline/service_logs.md +++ b/docs/reference/commandline/service_logs.md @@ -79,7 +79,8 @@ fraction of a second no more than nine digits long. You can combine the * [service create](service_create.md) * [service inspect](service_inspect.md) * [service ls](service_ls.md) -* [service rm](service_rm.md) -* [service scale](service_scale.md) * [service ps](service_ps.md) +* [service rm](service_rm.md) +* [service rollback](service_rollback.md) +* [service scale](service_scale.md) * [service update](service_update.md) diff --git a/docs/reference/commandline/service_ls.md b/docs/reference/commandline/service_ls.md index 1625dd254d..4ee551be3c 100644 --- a/docs/reference/commandline/service_ls.md +++ b/docs/reference/commandline/service_ls.md @@ -158,7 +158,8 @@ fm6uf97exkul: global 5/5 * [service create](service_create.md) * [service inspect](service_inspect.md) * [service logs](service_logs.md) -* [service rm](service_rm.md) -* [service scale](service_scale.md) * [service ps](service_ps.md) +* [service rm](service_rm.md) +* [service rollback](service_rollback.md) +* [service scale](service_scale.md) * [service update](service_update.md) diff --git a/docs/reference/commandline/service_ps.md b/docs/reference/commandline/service_ps.md index 51e3a28afe..88a963ae68 100644 --- a/docs/reference/commandline/service_ps.md +++ b/docs/reference/commandline/service_ps.md @@ -190,5 +190,6 @@ top.3: busybox * [service logs](service_logs.md) * [service ls](service_ls.md) * [service rm](service_rm.md) +* [service rollback](service_rollback.md) * [service scale](service_scale.md) * [service update](service_update.md) diff --git a/docs/reference/commandline/service_rm.md b/docs/reference/commandline/service_rm.md index 742fc38c80..1e3ada3d7d 100644 --- a/docs/reference/commandline/service_rm.md +++ b/docs/reference/commandline/service_rm.md @@ -55,6 +55,7 @@ ID NAME MODE REPLICAS IMAGE * [service inspect](service_inspect.md) * [service logs](service_logs.md) * [service ls](service_ls.md) -* [service scale](service_scale.md) * [service ps](service_ps.md) +* [service rollback](service_rollback.md) +* [service scale](service_scale.md) * [service update](service_update.md) diff --git a/docs/reference/commandline/service_rollback.md b/docs/reference/commandline/service_rollback.md new file mode 100644 index 0000000000..94ac2607e3 --- /dev/null +++ b/docs/reference/commandline/service_rollback.md @@ -0,0 +1,94 @@ +--- +title: "service rollback" +description: "The service rollback command description and usage" +keywords: "service, rollback" +--- + + + +# service rollback + +```markdown +Usage: docker service rollback SERVICE + +Revert changes to a service's configuration + +Options: + -d, --detach Exit immediately instead of waiting for the service to converge (default true) + --help Print usage + -q, --quiet Suppress progress output +``` + +## Description + +Roll back a specified service to its previous version from the swarm. This command must be run +targeting a manager node. + +## Examples + +### Roll back to the previous version of a service + +Use the `docker service rollback` command to roll back to the previous version +of a service. After executing this command, the service is reverted to the +configuration that was in place before the most recent `docker service update` +command. + +The following example creates a service with a single replica, updates the +service to use three replicas, and then rolls back the service to the +previous version, having one replica. + +Create a service with a single replica: + +```bash +$ docker service create --name my-service -p 8080:80 nginx:alpine +``` + +Confirm that the service is running with a single replica: + +```bash +$ docker service ls + +ID NAME MODE REPLICAS IMAGE PORTS +xbw728mf6q0d my-service replicated 1/1 nginx:alpine *:8080->80/tcp +``` + +Update the service to use three replicas: + +```bash +$ docker service update --replicas=3 my-service + +$ docker service ls + +ID NAME MODE REPLICAS IMAGE PORTS +xbw728mf6q0d my-service replicated 3/3 nginx:alpine *:8080->80/tcp +``` + +Now roll back the service to its previous version, and confirm it is +running a single replica again: + +```bash +$ docker service rollback my-service + +$ docker service ls + +ID NAME MODE REPLICAS IMAGE PORTS +xbw728mf6q0d my-service replicated 1/1 nginx:alpine *:8080->80/tcp +``` + +## Related commands + +* [service create](service_create.md) +* [service inspect](service_inspect.md) +* [service logs](service_logs.md) +* [service ls](service_ls.md) +* [service ps](service_ps.md) +* [service rm](service_rm.md) +* [service scale](service_scale.md) +* [service update](service_update.md) diff --git a/docs/reference/commandline/service_scale.md b/docs/reference/commandline/service_scale.md index d16c850480..17a06a3ba3 100644 --- a/docs/reference/commandline/service_scale.md +++ b/docs/reference/commandline/service_scale.md @@ -101,5 +101,6 @@ ID NAME MODE REPLICAS IMAGE * [service logs](service_logs.md) * [service ls](service_ls.md) * [service rm](service_rm.md) +* [service rollback](service_rollback.md) * [service ps](service_ps.md) * [service update](service_update.md) diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index 32cc8abed2..ffadb54955 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -174,9 +174,9 @@ $ docker service update --mount-rm /somewhere myservice myservice ``` -### Rolling back to the previous version of a service +### Roll back to the previous version of a service -Use the `--rollback` option to roll back to the previous version of the service. +Use the `--rollback` option to roll back to the previous version of the service. This will revert the service to the configuration that was in place before the most recent `docker service update` command. @@ -193,7 +193,7 @@ ID NAME MODE REPLICAS IMAGE 80bvrzp6vxf3 web replicated 0/5 nginx:alpine ``` -Roll back the `web` service... +Roll back the `web` service... ```bash $ docker service update --rollback web @@ -266,4 +266,5 @@ See [`service create`](./service_create.md#templating) for the reference. * [service ls](service_ls.md) * [service ps](service_ps.md) * [service rm](service_rm.md) +* [service rollback](service_rollback.md) * [service scale](service_scale.md)