Bladeren bron

Implement server-side rollback, for daemon versions that support this

Server-side rollback can take advantage of the rollback-specific update
parameters, instead of being treated as a normal update that happens to
go back to a previous version of the spec.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Aaron Lehmann 8 jaren geleden
bovenliggende
commit
f9bd8ec8b2

+ 1 - 1
api/server/router/swarm/backend.go

@@ -19,7 +19,7 @@ type Backend interface {
 	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
 	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
 	GetService(string) (types.Service, error)
 	GetService(string) (types.Service, error)
 	CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
 	CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
-	UpdateService(string, uint64, types.ServiceSpec, string, string) (*basictypes.ServiceUpdateResponse, error)
+	UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error)
 	RemoveService(string) error
 	RemoveService(string) error
 	ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
 	ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
 	GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
 	GetNodes(basictypes.NodeListOptions) ([]types.Node, error)

+ 6 - 4
api/server/router/swarm/cluster_routes.go

@@ -192,12 +192,14 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
 		return errors.NewBadRequestError(err)
 		return errors.NewBadRequestError(err)
 	}
 	}
 
 
-	// Get returns "" if the header does not exist
-	encodedAuth := r.Header.Get("X-Registry-Auth")
+	var flags basictypes.ServiceUpdateOptions
 
 
-	registryAuthFrom := r.URL.Query().Get("registryAuthFrom")
+	// Get returns "" if the header does not exist
+	flags.EncodedRegistryAuth = r.Header.Get("X-Registry-Auth")
+	flags.RegistryAuthFrom = r.URL.Query().Get("registryAuthFrom")
+	flags.Rollback = r.URL.Query().Get("rollback")
 
 
-	resp, err := sr.backend.UpdateService(vars["id"], version, service, encodedAuth, registryAuthFrom)
+	resp, err := sr.backend.UpdateService(vars["id"], version, service, flags)
 	if err != nil {
 	if err != nil {
 		logrus.Errorf("Error updating service %s: %v", vars["id"], err)
 		logrus.Errorf("Error updating service %s: %v", vars["id"], err)
 		return err
 		return err

+ 6 - 0
api/swagger.yaml

@@ -7631,6 +7631,12 @@ paths:
   parameter indicates where to find registry authorization credentials. The
   parameter indicates where to find registry authorization credentials. The
   valid values are `spec` and `previous-spec`."
   valid values are `spec` and `previous-spec`."
           default: "spec"
           default: "spec"
+        - name: "rollback"
+          in: "query"
+          type: "string"
+          description: "Set to this parameter to `previous` to cause a
+  server-side rollback to the previous service spec. The supplied spec will be
+  ignored in this case."
         - name: "X-Registry-Auth"
         - name: "X-Registry-Auth"
           in: "header"
           in: "header"
           description: "A base64-encoded auth configuration for pulling from private registries. [See the authentication section for details.](#section/Authentication)"
           description: "A base64-encoded auth configuration for pulling from private registries. [See the authentication section for details.](#section/Authentication)"

+ 6 - 0
api/types/client.go

@@ -320,6 +320,12 @@ type ServiceUpdateOptions struct {
 	// credentials if they are not given in EncodedRegistryAuth. Valid
 	// credentials if they are not given in EncodedRegistryAuth. Valid
 	// values are "spec" and "previous-spec".
 	// values are "spec" and "previous-spec".
 	RegistryAuthFrom string
 	RegistryAuthFrom string
+
+	// Rollback indicates whether a server-side rollback should be
+	// performed. When this is set, the provided spec will be ignored.
+	// The valid values are "previous" and "none". An empty value is the
+	// same as "none".
+	Rollback string
 }
 }
 
 
 // ServiceListOptions holds parameters to list  services with.
 // ServiceListOptions holds parameters to list  services with.

+ 38 - 5
cli/command/service/update.go

@@ -1,6 +1,7 @@
 package service
 package service
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"sort"
 	"sort"
 	"strings"
 	"strings"
@@ -10,6 +11,7 @@ import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/container"
 	mounttypes "github.com/docker/docker/api/types/mount"
 	mounttypes "github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/client"
@@ -95,7 +97,6 @@ func newListOptsVar() *opts.ListOpts {
 func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error {
 func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error {
 	apiClient := dockerCli.Client()
 	apiClient := dockerCli.Client()
 	ctx := context.Background()
 	ctx := context.Background()
-	updateOpts := types.ServiceUpdateOptions{}
 
 
 	service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID)
 	service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID)
 	if err != nil {
 	if err != nil {
@@ -107,12 +108,44 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
 		return err
 		return err
 	}
 	}
 
 
+	// There are two ways to do user-requested rollback. The old way is
+	// client-side, but with a sufficiently recent daemon we prefer
+	// server-side, because it will honor the rollback parameters.
+	var (
+		clientSideRollback bool
+		serverSideRollback bool
+	)
+
 	spec := &service.Spec
 	spec := &service.Spec
 	if rollback {
 	if rollback {
-		spec = service.PreviousSpec
-		if spec == nil {
-			return fmt.Errorf("service does not have a previous specification to roll back to")
+		// Rollback can't be combined with other flags.
+		otherFlagsPassed := false
+		flags.VisitAll(func(f *pflag.Flag) {
+			if f.Name == "rollback" {
+				return
+			}
+			if flags.Changed(f.Name) {
+				otherFlagsPassed = true
+			}
+		})
+		if otherFlagsPassed {
+			return errors.New("other flags may not be combined with --rollback")
 		}
 		}
+
+		if versions.LessThan(dockerCli.Client().ClientVersion(), "1.27") {
+			clientSideRollback = true
+			spec = service.PreviousSpec
+			if spec == nil {
+				return fmt.Errorf("service does not have a previous specification to roll back to")
+			}
+		} else {
+			serverSideRollback = true
+		}
+	}
+
+	updateOpts := types.ServiceUpdateOptions{}
+	if serverSideRollback {
+		updateOpts.Rollback = "previous"
 	}
 	}
 
 
 	err = updateService(flags, spec)
 	err = updateService(flags, spec)
@@ -147,7 +180,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
 			return err
 			return err
 		}
 		}
 		updateOpts.EncodedRegistryAuth = encodedAuth
 		updateOpts.EncodedRegistryAuth = encodedAuth
-	} else if rollback {
+	} else if clientSideRollback {
 		updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec
 		updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec
 	} else {
 	} else {
 		updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec
 		updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec

+ 4 - 0
client/service_update.go

@@ -27,6 +27,10 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
 		query.Set("registryAuthFrom", options.RegistryAuthFrom)
 		query.Set("registryAuthFrom", options.RegistryAuthFrom)
 	}
 	}
 
 
+	if options.Rollback != "" {
+		query.Set("rollback", options.Rollback)
+	}
+
 	query.Set("version", strconv.FormatUint(version.Index, 10))
 	query.Set("version", strconv.FormatUint(version.Index, 10))
 
 
 	var response types.ServiceUpdateResponse
 	var response types.ServiceUpdateResponse

+ 1 - 1
daemon/cluster/convert/service.go

@@ -176,7 +176,7 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
 	}
 	}
 	spec.Rollback, err = updateConfigToGRPC(s.RollbackConfig)
 	spec.Rollback, err = updateConfigToGRPC(s.RollbackConfig)
 	if err != nil {
 	if err != nil {
-		return swarmapi.Servicepec{}, err
+		return swarmapi.ServiceSpec{}, err
 	}
 	}
 
 
 	if s.EndpointSpec != nil {
 	if s.EndpointSpec != nil {

+ 14 - 2
daemon/cluster/services.go

@@ -132,7 +132,7 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity
 }
 }
 
 
 // UpdateService updates existing service to match new properties.
 // UpdateService updates existing service to match new properties.
-func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, encodedAuth string, registryAuthFrom string) (*apitypes.ServiceUpdateResponse, error) {
+func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, flags apitypes.ServiceUpdateOptions) (*apitypes.ServiceUpdateResponse, error) {
 	var resp *apitypes.ServiceUpdateResponse
 	var resp *apitypes.ServiceUpdateResponse
 
 
 	err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
 	err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
@@ -157,13 +157,14 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
 			return errors.New("service does not use container tasks")
 			return errors.New("service does not use container tasks")
 		}
 		}
 
 
+		encodedAuth := flags.EncodedRegistryAuth
 		if encodedAuth != "" {
 		if encodedAuth != "" {
 			newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
 			newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
 		} else {
 		} else {
 			// this is needed because if the encodedAuth isn't being updated then we
 			// this is needed because if the encodedAuth isn't being updated then we
 			// shouldn't lose it, and continue to use the one that was already present
 			// shouldn't lose it, and continue to use the one that was already present
 			var ctnr *swarmapi.ContainerSpec
 			var ctnr *swarmapi.ContainerSpec
-			switch registryAuthFrom {
+			switch flags.RegistryAuthFrom {
 			case apitypes.RegistryAuthFromSpec, "":
 			case apitypes.RegistryAuthFromSpec, "":
 				ctnr = currentService.Spec.Task.GetContainer()
 				ctnr = currentService.Spec.Task.GetContainer()
 			case apitypes.RegistryAuthFromPreviousSpec:
 			case apitypes.RegistryAuthFromPreviousSpec:
@@ -208,6 +209,16 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
 			}
 			}
 		}
 		}
 
 
+		var rollback swarmapi.UpdateServiceRequest_Rollback
+		switch flags.Rollback {
+		case "", "none":
+			rollback = swarmapi.UpdateServiceRequest_NONE
+		case "previous":
+			rollback = swarmapi.UpdateServiceRequest_PREVIOUS
+		default:
+			return fmt.Errorf("unrecognized rollback option %s", flags.Rollback)
+		}
+
 		_, err = state.controlClient.UpdateService(
 		_, err = state.controlClient.UpdateService(
 			ctx,
 			ctx,
 			&swarmapi.UpdateServiceRequest{
 			&swarmapi.UpdateServiceRequest{
@@ -216,6 +227,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
 				ServiceVersion: &swarmapi.Version{
 				ServiceVersion: &swarmapi.Version{
 					Index: version,
 					Index: version,
 				},
 				},
+				Rollback: rollback,
 			},
 			},
 		)
 		)
 		return err
 		return err

+ 4 - 7
integration-cli/docker_api_swarm_service_test.go

@@ -138,6 +138,7 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdate(c *check.C) {
 	// create service
 	// create service
 	instances := 5
 	instances := 5
 	parallelism := 2
 	parallelism := 2
+	rollbackParallelism := 3
 	id := daemons[0].CreateService(c, serviceForUpdate, setInstances(instances))
 	id := daemons[0].CreateService(c, serviceForUpdate, setInstances(instances))
 
 
 	// wait for tasks ready
 	// wait for tasks ready
@@ -161,19 +162,15 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdate(c *check.C) {
 		map[string]int{image2: instances})
 		map[string]int{image2: instances})
 
 
 	// Roll back to the previous version. This uses the CLI because
 	// Roll back to the previous version. This uses the CLI because
-	// rollback is a client-side operation.
+	// rollback used to be a client-side operation.
 	out, err := daemons[0].Cmd("service", "update", "--rollback", id)
 	out, err := daemons[0].Cmd("service", "update", "--rollback", id)
 	c.Assert(err, checker.IsNil, check.Commentf(out))
 	c.Assert(err, checker.IsNil, check.Commentf(out))
 
 
 	// first batch
 	// first batch
 	waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals,
 	waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals,
-		map[string]int{image2: instances - parallelism, image1: parallelism})
+		map[string]int{image2: instances - rollbackParallelism, image1: rollbackParallelism})
 
 
 	// 2nd batch
 	// 2nd batch
-	waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals,
-		map[string]int{image2: instances - 2*parallelism, image1: 2 * parallelism})
-
-	// 3nd batch
 	waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals,
 	waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals,
 		map[string]int{image1: instances})
 		map[string]int{image1: instances})
 }
 }
@@ -210,7 +207,7 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesFailedUpdate(c *check.C) {
 	c.Assert(v, checker.Equals, instances-2)
 	c.Assert(v, checker.Equals, instances-2)
 
 
 	// Roll back to the previous version. This uses the CLI because
 	// Roll back to the previous version. This uses the CLI because
-	// rollback is a client-side operation.
+	// rollback used to be a client-side operation.
 	out, err := daemons[0].Cmd("service", "update", "--rollback", id)
 	out, err := daemons[0].Cmd("service", "update", "--rollback", id)
 	c.Assert(err, checker.IsNil, check.Commentf(out))
 	c.Assert(err, checker.IsNil, check.Commentf(out))
 
 

+ 5 - 0
integration-cli/docker_api_swarm_test.go

@@ -556,6 +556,11 @@ func serviceForUpdate(s *swarm.Service) {
 			Delay:         4 * time.Second,
 			Delay:         4 * time.Second,
 			FailureAction: swarm.UpdateFailureActionContinue,
 			FailureAction: swarm.UpdateFailureActionContinue,
 		},
 		},
+		RollbackConfig: &swarm.UpdateConfig{
+			Parallelism:   3,
+			Delay:         4 * time.Second,
+			FailureAction: swarm.UpdateFailureActionContinue,
+		},
 	}
 	}
 	s.Spec.Name = "updatetest"
 	s.Spec.Name = "updatetest"
 }
 }