Merge pull request #205 from redpanda/rollback
Add 'docker service rollback' subcommand
Upstream-commit: 3c7ede6a68
Component: cli
This commit is contained in:
@ -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,
|
||||
|
||||
@ -26,6 +26,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
newScaleCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
newLogsCommand(dockerCli),
|
||||
newRollbackCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -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{}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
65
components/cli/cli/command/service/rollback.go
Normal file
65
components/cli/cli/command/service/rollback.go
Normal 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)
|
||||
}
|
||||
104
components/cli/cli/command/service/rollback_test.go
Normal file
104
components/cli/cli/command/service/rollback_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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{})
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user