Browse code

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 authored on 2017/02/17 02:27:01
Showing 10 changed files
... ...
@@ -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
 }