Skip to content

Commit

Permalink
Implement server-side rollback, for daemon versions that support this
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
aaronlehmann committed Mar 4, 2017
1 parent 3a88a24 commit f9bd8ec
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 20 deletions.
2 changes: 1 addition & 1 deletion api/server/router/swarm/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Backend interface {
GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
GetService(string) (types.Service, 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
ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
Expand Down
10 changes: 6 additions & 4 deletions api/server/router/swarm/cluster_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,14 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
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 {
logrus.Errorf("Error updating service %s: %v", vars["id"], err)
return err
Expand Down
6 changes: 6 additions & 0 deletions api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7631,6 +7631,12 @@ paths:
parameter indicates where to find registry authorization credentials. The
valid values are `spec` and `previous-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"
in: "header"
description: "A base64-encoded auth configuration for pulling from private registries. [See the authentication section for details.](#section/Authentication)"
Expand Down
6 changes: 6 additions & 0 deletions api/types/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ type ServiceUpdateOptions struct {
// credentials if they are not given in EncodedRegistryAuth. Valid
// values are "spec" and "previous-spec".
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.
Expand Down
43 changes: 38 additions & 5 deletions cli/command/service/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package service

import (
"errors"
"fmt"
"sort"
"strings"
Expand All @@ -10,6 +11,7 @@ import (
"github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/client"
Expand Down Expand Up @@ -95,7 +97,6 @@ func newListOptsVar() *opts.ListOpts {
func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error {
apiClient := dockerCli.Client()
ctx := context.Background()
updateOpts := types.ServiceUpdateOptions{}

service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID)
if err != nil {
Expand All @@ -107,12 +108,44 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
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
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)
Expand Down Expand Up @@ -147,7 +180,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
return err
}
updateOpts.EncodedRegistryAuth = encodedAuth
} else if rollback {
} else if clientSideRollback {
updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec
} else {
updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec
Expand Down
4 changes: 4 additions & 0 deletions client/service_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
query.Set("registryAuthFrom", options.RegistryAuthFrom)
}

if options.Rollback != "" {
query.Set("rollback", options.Rollback)
}

query.Set("version", strconv.FormatUint(version.Index, 10))

var response types.ServiceUpdateResponse
Expand Down
2 changes: 1 addition & 1 deletion daemon/cluster/convert/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
}
spec.Rollback, err = updateConfigToGRPC(s.RollbackConfig)
if err != nil {
return swarmapi.Servicepec{}, err
return swarmapi.ServiceSpec{}, err
}

if s.EndpointSpec != nil {
Expand Down
16 changes: 14 additions & 2 deletions daemon/cluster/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity
}

// 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

err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
Expand All @@ -157,13 +157,14 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
return errors.New("service does not use container tasks")
}

encodedAuth := flags.EncodedRegistryAuth
if encodedAuth != "" {
newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
} else {
// 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
var ctnr *swarmapi.ContainerSpec
switch registryAuthFrom {
switch flags.RegistryAuthFrom {
case apitypes.RegistryAuthFromSpec, "":
ctnr = currentService.Spec.Task.GetContainer()
case apitypes.RegistryAuthFromPreviousSpec:
Expand Down Expand Up @@ -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(
ctx,
&swarmapi.UpdateServiceRequest{
Expand All @@ -216,6 +227,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
ServiceVersion: &swarmapi.Version{
Index: version,
},
Rollback: rollback,
},
)
return err
Expand Down
11 changes: 4 additions & 7 deletions integration-cli/docker_api_swarm_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdate(c *check.C) {
// create service
instances := 5
parallelism := 2
rollbackParallelism := 3
id := daemons[0].CreateService(c, serviceForUpdate, setInstances(instances))

// wait for tasks ready
Expand All @@ -161,19 +162,15 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdate(c *check.C) {
map[string]int{image2: instances})

// 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)
c.Assert(err, checker.IsNil, check.Commentf(out))

// first batch
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
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,
map[string]int{image1: instances})
}
Expand Down Expand Up @@ -210,7 +207,7 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesFailedUpdate(c *check.C) {
c.Assert(v, checker.Equals, instances-2)

// 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)
c.Assert(err, checker.IsNil, check.Commentf(out))

Expand Down
5 changes: 5 additions & 0 deletions integration-cli/docker_api_swarm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,11 @@ func serviceForUpdate(s *swarm.Service) {
Delay: 4 * time.Second,
FailureAction: swarm.UpdateFailureActionContinue,
},
RollbackConfig: &swarm.UpdateConfig{
Parallelism: 3,
Delay: 4 * time.Second,
FailureAction: swarm.UpdateFailureActionContinue,
},
}
s.Spec.Name = "updatetest"
}
Expand Down

0 comments on commit f9bd8ec

Please sign in to comment.