Merge pull request #205 from redpanda/rollback

Add 'docker service rollback' subcommand
This commit is contained in:
Sebastiaan van Stijn 2017-08-19 15:56:14 +02:00 committed by GitHub
commit 3c7ede6a68
19 changed files with 323 additions and 30 deletions

View File

@ -5,17 +5,18 @@ import (
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"golang.org/x/net/context" "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 { type fakeClient struct {
client.Client 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) { func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
if f.serviceListFunc != nil {
return f.serviceListFunc(ctx, options)
}
return nil, nil return nil, nil
} }
@ -23,10 +24,30 @@ func (f *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions
return nil, nil 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 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 { func newService(id string, name string) swarm.Service {
return swarm.Service{ return swarm.Service{
ID: id, ID: id,

View File

@ -26,6 +26,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
newScaleCommand(dockerCli), newScaleCommand(dockerCli),
newUpdateCommand(dockerCli), newUpdateCommand(dockerCli),
newLogsCommand(dockerCli), newLogsCommand(dockerCli),
newRollbackCommand(dockerCli),
) )
return cmd return cmd
} }

View File

@ -12,7 +12,7 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { func newCreateCommand(dockerCli command.Cli) *cobra.Command {
opts := newServiceOptions() opts := newServiceOptions()
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -62,7 +62,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
return cmd 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() apiClient := dockerCli.Client()
createOpts := types.ServiceCreateOptions{} createOpts := types.ServiceCreateOptions{}

View File

@ -15,7 +15,7 @@ import (
// waitOnService waits for the service to converge. It outputs a progress bar, // waitOnService waits for the service to converge. It outputs a progress bar,
// if appropriate based on the CLI flags. // 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) errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()

View File

@ -20,7 +20,7 @@ type inspectOptions struct {
pretty bool pretty bool
} }
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { func newInspectCommand(dockerCli command.Cli) *cobra.Command {
var opts inspectOptions var opts inspectOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -43,7 +43,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
return cmd return cmd
} }
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { func runInspect(dockerCli command.Cli, opts inspectOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background() ctx := context.Background()

View File

@ -11,7 +11,7 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "rm SERVICE [SERVICE...]", Use: "rm SERVICE [SERVICE...]",
@ -27,7 +27,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
return cmd return cmd
} }
func runRemove(dockerCli *command.DockerCli, sids []string) error { func runRemove(dockerCli command.Cli, sids []string) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background() ctx := context.Background()

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -19,7 +19,7 @@ type scaleOptions struct {
detach bool detach bool
} }
func newScaleCommand(dockerCli *command.DockerCli) *cobra.Command { func newScaleCommand(dockerCli command.Cli) *cobra.Command {
options := &scaleOptions{} options := &scaleOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -54,7 +54,7 @@ func scaleArgs(cmd *cobra.Command, args []string) error {
return nil 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 errs []string
var serviceIDs []string var serviceIDs []string
ctx := context.Background() ctx := context.Background()
@ -96,7 +96,7 @@ func runScale(dockerCli *command.DockerCli, flags *pflag.FlagSet, options *scale
return errors.Errorf(strings.Join(errs, "\n")) 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() client := dockerCli.Client()
service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})

View File

@ -22,7 +22,7 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
options := newServiceOptions() options := newServiceOptions()
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -103,7 +103,7 @@ func newListOptsVar() *opts.ListOpts {
} }
// nolint: gocyclo // 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() apiClient := dockerCli.Client()
ctx := context.Background() ctx := context.Background()

View File

@ -875,9 +875,10 @@ x3ti0erg11rjpg64m75kej2mz-hosttempl
* [service inspect](service_inspect.md) * [service inspect](service_inspect.md)
* [service logs](service_logs.md) * [service logs](service_logs.md)
* [service ls](service_ls.md) * [service ls](service_ls.md)
* [service rm](service_rm.md)
* [service scale](service_scale.md)
* [service ps](service_ps.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) * [service update](service_update.md)
<style>table tr > td:first-child { white-space: nowrap;}</style> <style>table tr > td:first-child { white-space: nowrap;}</style>

View File

@ -164,7 +164,8 @@ $ docker service inspect --format='{{.Spec.Mode.Replicated.Replicas}}' redis
* [service create](service_create.md) * [service create](service_create.md)
* [service logs](service_logs.md) * [service logs](service_logs.md)
* [service ls](service_ls.md) * [service ls](service_ls.md)
* [service rm](service_rm.md)
* [service scale](service_scale.md)
* [service ps](service_ps.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) * [service update](service_update.md)

View File

@ -79,7 +79,8 @@ fraction of a second no more than nine digits long. You can combine the
* [service create](service_create.md) * [service create](service_create.md)
* [service inspect](service_inspect.md) * [service inspect](service_inspect.md)
* [service ls](service_ls.md) * [service ls](service_ls.md)
* [service rm](service_rm.md)
* [service scale](service_scale.md)
* [service ps](service_ps.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) * [service update](service_update.md)

View File

@ -158,7 +158,8 @@ fm6uf97exkul: global 5/5
* [service create](service_create.md) * [service create](service_create.md)
* [service inspect](service_inspect.md) * [service inspect](service_inspect.md)
* [service logs](service_logs.md) * [service logs](service_logs.md)
* [service rm](service_rm.md)
* [service scale](service_scale.md)
* [service ps](service_ps.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) * [service update](service_update.md)

View File

@ -190,5 +190,6 @@ top.3: busybox
* [service logs](service_logs.md) * [service logs](service_logs.md)
* [service ls](service_ls.md) * [service ls](service_ls.md)
* [service rm](service_rm.md) * [service rm](service_rm.md)
* [service rollback](service_rollback.md)
* [service scale](service_scale.md) * [service scale](service_scale.md)
* [service update](service_update.md) * [service update](service_update.md)

View File

@ -55,6 +55,7 @@ ID NAME MODE REPLICAS IMAGE
* [service inspect](service_inspect.md) * [service inspect](service_inspect.md)
* [service logs](service_logs.md) * [service logs](service_logs.md)
* [service ls](service_ls.md) * [service ls](service_ls.md)
* [service scale](service_scale.md)
* [service ps](service_ps.md) * [service ps](service_ps.md)
* [service rollback](service_rollback.md)
* [service scale](service_scale.md)
* [service update](service_update.md) * [service update](service_update.md)

View File

@ -0,0 +1,94 @@
---
title: "service rollback"
description: "The service rollback command description and usage"
keywords: "service, rollback"
---
<!-- This file is maintained within the docker/cli Github
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# 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)

View File

@ -101,5 +101,6 @@ ID NAME MODE REPLICAS IMAGE
* [service logs](service_logs.md) * [service logs](service_logs.md)
* [service ls](service_ls.md) * [service ls](service_ls.md)
* [service rm](service_rm.md) * [service rm](service_rm.md)
* [service rollback](service_rollback.md)
* [service ps](service_ps.md) * [service ps](service_ps.md)
* [service update](service_update.md) * [service update](service_update.md)

View File

@ -174,9 +174,9 @@ $ docker service update --mount-rm /somewhere myservice
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. 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 80bvrzp6vxf3 web replicated 0/5 nginx:alpine
``` ```
Roll back the `web` service... Roll back the `web` service...
```bash ```bash
$ docker service update --rollback web $ 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 ls](service_ls.md)
* [service ps](service_ps.md) * [service ps](service_ps.md)
* [service rm](service_rm.md) * [service rm](service_rm.md)
* [service rollback](service_rollback.md)
* [service scale](service_scale.md) * [service scale](service_scale.md)