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>
| ... | ... |
@@ -19,7 +19,7 @@ type Backend interface {
|
| 19 | 19 |
GetServices(basictypes.ServiceListOptions) ([]types.Service, error) |
| 20 | 20 |
GetService(string) (types.Service, error) |
| 21 | 21 |
CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error) |
| 22 |
- UpdateService(string, uint64, types.ServiceSpec, string, string) (*basictypes.ServiceUpdateResponse, error) |
|
| 22 |
+ UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error) |
|
| 23 | 23 |
RemoveService(string) error |
| 24 | 24 |
ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
|
| 25 | 25 |
GetNodes(basictypes.NodeListOptions) ([]types.Node, error) |
| ... | ... |
@@ -192,12 +192,14 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter, |
| 192 | 192 |
return errors.NewBadRequestError(err) |
| 193 | 193 |
} |
| 194 | 194 |
|
| 195 |
- // Get returns "" if the header does not exist |
|
| 196 |
- encodedAuth := r.Header.Get("X-Registry-Auth")
|
|
| 195 |
+ var flags basictypes.ServiceUpdateOptions |
|
| 197 | 196 |
|
| 198 |
- registryAuthFrom := r.URL.Query().Get("registryAuthFrom")
|
|
| 197 |
+ // Get returns "" if the header does not exist |
|
| 198 |
+ flags.EncodedRegistryAuth = r.Header.Get("X-Registry-Auth")
|
|
| 199 |
+ flags.RegistryAuthFrom = r.URL.Query().Get("registryAuthFrom")
|
|
| 200 |
+ flags.Rollback = r.URL.Query().Get("rollback")
|
|
| 199 | 201 |
|
| 200 |
- resp, err := sr.backend.UpdateService(vars["id"], version, service, encodedAuth, registryAuthFrom) |
|
| 202 |
+ resp, err := sr.backend.UpdateService(vars["id"], version, service, flags) |
|
| 201 | 203 |
if err != nil {
|
| 202 | 204 |
logrus.Errorf("Error updating service %s: %v", vars["id"], err)
|
| 203 | 205 |
return err |
| ... | ... |
@@ -7631,6 +7631,12 @@ paths: |
| 7631 | 7631 |
parameter indicates where to find registry authorization credentials. The |
| 7632 | 7632 |
valid values are `spec` and `previous-spec`." |
| 7633 | 7633 |
default: "spec" |
| 7634 |
+ - name: "rollback" |
|
| 7635 |
+ in: "query" |
|
| 7636 |
+ type: "string" |
|
| 7637 |
+ description: "Set to this parameter to `previous` to cause a |
|
| 7638 |
+ server-side rollback to the previous service spec. The supplied spec will be |
|
| 7639 |
+ ignored in this case." |
|
| 7634 | 7640 |
- name: "X-Registry-Auth" |
| 7635 | 7641 |
in: "header" |
| 7636 | 7642 |
description: "A base64-encoded auth configuration for pulling from private registries. [See the authentication section for details.](#section/Authentication)" |
| ... | ... |
@@ -320,6 +320,12 @@ type ServiceUpdateOptions struct {
|
| 320 | 320 |
// credentials if they are not given in EncodedRegistryAuth. Valid |
| 321 | 321 |
// values are "spec" and "previous-spec". |
| 322 | 322 |
RegistryAuthFrom string |
| 323 |
+ |
|
| 324 |
+ // Rollback indicates whether a server-side rollback should be |
|
| 325 |
+ // performed. When this is set, the provided spec will be ignored. |
|
| 326 |
+ // The valid values are "previous" and "none". An empty value is the |
|
| 327 |
+ // same as "none". |
|
| 328 |
+ Rollback string |
|
| 323 | 329 |
} |
| 324 | 330 |
|
| 325 | 331 |
// ServiceListOptions holds parameters to list services with. |
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package service |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "errors" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"sort" |
| 6 | 7 |
"strings" |
| ... | ... |
@@ -10,6 +11,7 @@ import ( |
| 10 | 10 |
"github.com/docker/docker/api/types/container" |
| 11 | 11 |
mounttypes "github.com/docker/docker/api/types/mount" |
| 12 | 12 |
"github.com/docker/docker/api/types/swarm" |
| 13 |
+ "github.com/docker/docker/api/types/versions" |
|
| 13 | 14 |
"github.com/docker/docker/cli" |
| 14 | 15 |
"github.com/docker/docker/cli/command" |
| 15 | 16 |
"github.com/docker/docker/client" |
| ... | ... |
@@ -95,7 +97,6 @@ func newListOptsVar() *opts.ListOpts {
|
| 95 | 95 |
func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error {
|
| 96 | 96 |
apiClient := dockerCli.Client() |
| 97 | 97 |
ctx := context.Background() |
| 98 |
- updateOpts := types.ServiceUpdateOptions{}
|
|
| 99 | 98 |
|
| 100 | 99 |
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID) |
| 101 | 100 |
if err != nil {
|
| ... | ... |
@@ -107,12 +108,44 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str |
| 107 | 107 |
return err |
| 108 | 108 |
} |
| 109 | 109 |
|
| 110 |
+ // There are two ways to do user-requested rollback. The old way is |
|
| 111 |
+ // client-side, but with a sufficiently recent daemon we prefer |
|
| 112 |
+ // server-side, because it will honor the rollback parameters. |
|
| 113 |
+ var ( |
|
| 114 |
+ clientSideRollback bool |
|
| 115 |
+ serverSideRollback bool |
|
| 116 |
+ ) |
|
| 117 |
+ |
|
| 110 | 118 |
spec := &service.Spec |
| 111 | 119 |
if rollback {
|
| 112 |
- spec = service.PreviousSpec |
|
| 113 |
- if spec == nil {
|
|
| 114 |
- return fmt.Errorf("service does not have a previous specification to roll back to")
|
|
| 120 |
+ // Rollback can't be combined with other flags. |
|
| 121 |
+ otherFlagsPassed := false |
|
| 122 |
+ flags.VisitAll(func(f *pflag.Flag) {
|
|
| 123 |
+ if f.Name == "rollback" {
|
|
| 124 |
+ return |
|
| 125 |
+ } |
|
| 126 |
+ if flags.Changed(f.Name) {
|
|
| 127 |
+ otherFlagsPassed = true |
|
| 128 |
+ } |
|
| 129 |
+ }) |
|
| 130 |
+ if otherFlagsPassed {
|
|
| 131 |
+ return errors.New("other flags may not be combined with --rollback")
|
|
| 115 | 132 |
} |
| 133 |
+ |
|
| 134 |
+ if versions.LessThan(dockerCli.Client().ClientVersion(), "1.27") {
|
|
| 135 |
+ clientSideRollback = true |
|
| 136 |
+ spec = service.PreviousSpec |
|
| 137 |
+ if spec == nil {
|
|
| 138 |
+ return fmt.Errorf("service does not have a previous specification to roll back to")
|
|
| 139 |
+ } |
|
| 140 |
+ } else {
|
|
| 141 |
+ serverSideRollback = true |
|
| 142 |
+ } |
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ updateOpts := types.ServiceUpdateOptions{}
|
|
| 146 |
+ if serverSideRollback {
|
|
| 147 |
+ updateOpts.Rollback = "previous" |
|
| 116 | 148 |
} |
| 117 | 149 |
|
| 118 | 150 |
err = updateService(flags, spec) |
| ... | ... |
@@ -147,7 +180,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str |
| 147 | 147 |
return err |
| 148 | 148 |
} |
| 149 | 149 |
updateOpts.EncodedRegistryAuth = encodedAuth |
| 150 |
- } else if rollback {
|
|
| 150 |
+ } else if clientSideRollback {
|
|
| 151 | 151 |
updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec |
| 152 | 152 |
} else {
|
| 153 | 153 |
updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec |
| ... | ... |
@@ -27,6 +27,10 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version |
| 27 | 27 |
query.Set("registryAuthFrom", options.RegistryAuthFrom)
|
| 28 | 28 |
} |
| 29 | 29 |
|
| 30 |
+ if options.Rollback != "" {
|
|
| 31 |
+ query.Set("rollback", options.Rollback)
|
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 30 | 34 |
query.Set("version", strconv.FormatUint(version.Index, 10))
|
| 31 | 35 |
|
| 32 | 36 |
var response types.ServiceUpdateResponse |
| ... | ... |
@@ -176,7 +176,7 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
|
| 176 | 176 |
} |
| 177 | 177 |
spec.Rollback, err = updateConfigToGRPC(s.RollbackConfig) |
| 178 | 178 |
if err != nil {
|
| 179 |
- return swarmapi.Servicepec{}, err
|
|
| 179 |
+ return swarmapi.ServiceSpec{}, err
|
|
| 180 | 180 |
} |
| 181 | 181 |
|
| 182 | 182 |
if s.EndpointSpec != nil {
|
| ... | ... |
@@ -132,7 +132,7 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity |
| 132 | 132 |
} |
| 133 | 133 |
|
| 134 | 134 |
// UpdateService updates existing service to match new properties. |
| 135 |
-func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, encodedAuth string, registryAuthFrom string) (*apitypes.ServiceUpdateResponse, error) {
|
|
| 135 |
+func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, flags apitypes.ServiceUpdateOptions) (*apitypes.ServiceUpdateResponse, error) {
|
|
| 136 | 136 |
var resp *apitypes.ServiceUpdateResponse |
| 137 | 137 |
|
| 138 | 138 |
err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
|
| ... | ... |
@@ -157,13 +157,14 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ |
| 157 | 157 |
return errors.New("service does not use container tasks")
|
| 158 | 158 |
} |
| 159 | 159 |
|
| 160 |
+ encodedAuth := flags.EncodedRegistryAuth |
|
| 160 | 161 |
if encodedAuth != "" {
|
| 161 | 162 |
newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
| 162 | 163 |
} else {
|
| 163 | 164 |
// this is needed because if the encodedAuth isn't being updated then we |
| 164 | 165 |
// shouldn't lose it, and continue to use the one that was already present |
| 165 | 166 |
var ctnr *swarmapi.ContainerSpec |
| 166 |
- switch registryAuthFrom {
|
|
| 167 |
+ switch flags.RegistryAuthFrom {
|
|
| 167 | 168 |
case apitypes.RegistryAuthFromSpec, "": |
| 168 | 169 |
ctnr = currentService.Spec.Task.GetContainer() |
| 169 | 170 |
case apitypes.RegistryAuthFromPreviousSpec: |
| ... | ... |
@@ -208,6 +209,16 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ |
| 208 | 208 |
} |
| 209 | 209 |
} |
| 210 | 210 |
|
| 211 |
+ var rollback swarmapi.UpdateServiceRequest_Rollback |
|
| 212 |
+ switch flags.Rollback {
|
|
| 213 |
+ case "", "none": |
|
| 214 |
+ rollback = swarmapi.UpdateServiceRequest_NONE |
|
| 215 |
+ case "previous": |
|
| 216 |
+ rollback = swarmapi.UpdateServiceRequest_PREVIOUS |
|
| 217 |
+ default: |
|
| 218 |
+ return fmt.Errorf("unrecognized rollback option %s", flags.Rollback)
|
|
| 219 |
+ } |
|
| 220 |
+ |
|
| 211 | 221 |
_, err = state.controlClient.UpdateService( |
| 212 | 222 |
ctx, |
| 213 | 223 |
&swarmapi.UpdateServiceRequest{
|
| ... | ... |
@@ -216,6 +227,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ |
| 216 | 216 |
ServiceVersion: &swarmapi.Version{
|
| 217 | 217 |
Index: version, |
| 218 | 218 |
}, |
| 219 |
+ Rollback: rollback, |
|
| 219 | 220 |
}, |
| 220 | 221 |
) |
| 221 | 222 |
return err |
| ... | ... |
@@ -138,6 +138,7 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdate(c *check.C) {
|
| 138 | 138 |
// create service |
| 139 | 139 |
instances := 5 |
| 140 | 140 |
parallelism := 2 |
| 141 |
+ rollbackParallelism := 3 |
|
| 141 | 142 |
id := daemons[0].CreateService(c, serviceForUpdate, setInstances(instances)) |
| 142 | 143 |
|
| 143 | 144 |
// wait for tasks ready |
| ... | ... |
@@ -161,20 +162,16 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdate(c *check.C) {
|
| 161 | 161 |
map[string]int{image2: instances})
|
| 162 | 162 |
|
| 163 | 163 |
// Roll back to the previous version. This uses the CLI because |
| 164 |
- // rollback is a client-side operation. |
|
| 164 |
+ // rollback used to be a client-side operation. |
|
| 165 | 165 |
out, err := daemons[0].Cmd("service", "update", "--rollback", id)
|
| 166 | 166 |
c.Assert(err, checker.IsNil, check.Commentf(out)) |
| 167 | 167 |
|
| 168 | 168 |
// first batch |
| 169 | 169 |
waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals, |
| 170 |
- map[string]int{image2: instances - parallelism, image1: parallelism})
|
|
| 170 |
+ map[string]int{image2: instances - rollbackParallelism, image1: rollbackParallelism})
|
|
| 171 | 171 |
|
| 172 | 172 |
// 2nd batch |
| 173 | 173 |
waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals, |
| 174 |
- map[string]int{image2: instances - 2*parallelism, image1: 2 * parallelism})
|
|
| 175 |
- |
|
| 176 |
- // 3nd batch |
|
| 177 |
- waitAndAssert(c, defaultReconciliationTimeout, daemons[0].CheckRunningTaskImages, checker.DeepEquals, |
|
| 178 | 174 |
map[string]int{image1: instances})
|
| 179 | 175 |
} |
| 180 | 176 |
|
| ... | ... |
@@ -210,7 +207,7 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesFailedUpdate(c *check.C) {
|
| 210 | 210 |
c.Assert(v, checker.Equals, instances-2) |
| 211 | 211 |
|
| 212 | 212 |
// Roll back to the previous version. This uses the CLI because |
| 213 |
- // rollback is a client-side operation. |
|
| 213 |
+ // rollback used to be a client-side operation. |
|
| 214 | 214 |
out, err := daemons[0].Cmd("service", "update", "--rollback", id)
|
| 215 | 215 |
c.Assert(err, checker.IsNil, check.Commentf(out)) |
| 216 | 216 |
|
| ... | ... |
@@ -556,6 +556,11 @@ func serviceForUpdate(s *swarm.Service) {
|
| 556 | 556 |
Delay: 4 * time.Second, |
| 557 | 557 |
FailureAction: swarm.UpdateFailureActionContinue, |
| 558 | 558 |
}, |
| 559 |
+ RollbackConfig: &swarm.UpdateConfig{
|
|
| 560 |
+ Parallelism: 3, |
|
| 561 |
+ Delay: 4 * time.Second, |
|
| 562 |
+ FailureAction: swarm.UpdateFailureActionContinue, |
|
| 563 |
+ }, |
|
| 559 | 564 |
} |
| 560 | 565 |
s.Spec.Name = "updatetest" |
| 561 | 566 |
} |