Change `docker service update` to replace attributes of the target
service rather than augment them. One particular occurrence where the
previous behavior proved problematic is when trying to update a port
mapping: the merge semantics provided no way of removing published
ports, but strictly of adding more.
The utility merge* functions where renamed accordingly to update*.
Signed-off-by: Arnaud Porterie (icecrime) <arnaud.porterie@docker.com>
| ... | ... |
@@ -46,7 +46,7 @@ func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID stri |
| 46 | 46 |
return err |
| 47 | 47 |
} |
| 48 | 48 |
|
| 49 |
- err = mergeService(&service.Spec, flags) |
|
| 49 |
+ err = updateService(&service.Spec, flags) |
|
| 50 | 50 |
if err != nil {
|
| 51 | 51 |
return err |
| 52 | 52 |
} |
| ... | ... |
@@ -59,52 +59,52 @@ func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID stri |
| 59 | 59 |
return nil |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 |
-func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
|
| 62 |
+func updateService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
|
| 63 | 63 |
|
| 64 |
- mergeString := func(flag string, field *string) {
|
|
| 64 |
+ updateString := func(flag string, field *string) {
|
|
| 65 | 65 |
if flags.Changed(flag) {
|
| 66 | 66 |
*field, _ = flags.GetString(flag) |
| 67 | 67 |
} |
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 |
- mergeListOpts := func(flag string, field *[]string) {
|
|
| 70 |
+ updateListOpts := func(flag string, field *[]string) {
|
|
| 71 | 71 |
if flags.Changed(flag) {
|
| 72 | 72 |
value := flags.Lookup(flag).Value.(*opts.ListOpts) |
| 73 | 73 |
*field = value.GetAll() |
| 74 | 74 |
} |
| 75 | 75 |
} |
| 76 | 76 |
|
| 77 |
- mergeSlice := func(flag string, field *[]string) {
|
|
| 77 |
+ updateSlice := func(flag string, field *[]string) {
|
|
| 78 | 78 |
if flags.Changed(flag) {
|
| 79 | 79 |
*field, _ = flags.GetStringSlice(flag) |
| 80 | 80 |
} |
| 81 | 81 |
} |
| 82 | 82 |
|
| 83 |
- mergeInt64Value := func(flag string, field *int64) {
|
|
| 83 |
+ updateInt64Value := func(flag string, field *int64) {
|
|
| 84 | 84 |
if flags.Changed(flag) {
|
| 85 | 85 |
*field = flags.Lookup(flag).Value.(int64Value).Value() |
| 86 | 86 |
} |
| 87 | 87 |
} |
| 88 | 88 |
|
| 89 |
- mergeDuration := func(flag string, field *time.Duration) {
|
|
| 89 |
+ updateDuration := func(flag string, field *time.Duration) {
|
|
| 90 | 90 |
if flags.Changed(flag) {
|
| 91 | 91 |
*field, _ = flags.GetDuration(flag) |
| 92 | 92 |
} |
| 93 | 93 |
} |
| 94 | 94 |
|
| 95 |
- mergeDurationOpt := func(flag string, field *time.Duration) {
|
|
| 95 |
+ updateDurationOpt := func(flag string, field *time.Duration) {
|
|
| 96 | 96 |
if flags.Changed(flag) {
|
| 97 | 97 |
*field = *flags.Lookup(flag).Value.(*DurationOpt).Value() |
| 98 | 98 |
} |
| 99 | 99 |
} |
| 100 | 100 |
|
| 101 |
- mergeUint64 := func(flag string, field *uint64) {
|
|
| 101 |
+ updateUint64 := func(flag string, field *uint64) {
|
|
| 102 | 102 |
if flags.Changed(flag) {
|
| 103 | 103 |
*field, _ = flags.GetUint64(flag) |
| 104 | 104 |
} |
| 105 | 105 |
} |
| 106 | 106 |
|
| 107 |
- mergeUint64Opt := func(flag string, field *uint64) {
|
|
| 107 |
+ updateUint64Opt := func(flag string, field *uint64) {
|
|
| 108 | 108 |
if flags.Changed(flag) {
|
| 109 | 109 |
*field = *flags.Lookup(flag).Value.(*Uint64Opt).Value() |
| 110 | 110 |
} |
| ... | ... |
@@ -112,23 +112,23 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
| 112 | 112 |
|
| 113 | 113 |
cspec := &spec.TaskTemplate.ContainerSpec |
| 114 | 114 |
task := &spec.TaskTemplate |
| 115 |
- mergeString(flagName, &spec.Name) |
|
| 116 |
- mergeLabels(flags, &spec.Labels) |
|
| 117 |
- mergeString("image", &cspec.Image)
|
|
| 118 |
- mergeSlice("command", &cspec.Command)
|
|
| 119 |
- mergeSlice("arg", &cspec.Command)
|
|
| 120 |
- mergeListOpts("env", &cspec.Env)
|
|
| 121 |
- mergeString("workdir", &cspec.Dir)
|
|
| 122 |
- mergeString("user", &cspec.User)
|
|
| 123 |
- mergeMounts(flags, &cspec.Mounts) |
|
| 115 |
+ updateString(flagName, &spec.Name) |
|
| 116 |
+ updateLabels(flags, &spec.Labels) |
|
| 117 |
+ updateString("image", &cspec.Image)
|
|
| 118 |
+ updateSlice("command", &cspec.Command)
|
|
| 119 |
+ updateSlice("arg", &cspec.Command)
|
|
| 120 |
+ updateListOpts("env", &cspec.Env)
|
|
| 121 |
+ updateString("workdir", &cspec.Dir)
|
|
| 122 |
+ updateString("user", &cspec.User)
|
|
| 123 |
+ updateMounts(flags, &cspec.Mounts) |
|
| 124 | 124 |
|
| 125 | 125 |
if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) {
|
| 126 | 126 |
if task.Resources == nil {
|
| 127 | 127 |
task.Resources = &swarm.ResourceRequirements{}
|
| 128 | 128 |
} |
| 129 | 129 |
task.Resources.Limits = &swarm.Resources{}
|
| 130 |
- mergeInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs) |
|
| 131 |
- mergeInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes) |
|
| 130 |
+ updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs) |
|
| 131 |
+ updateInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes) |
|
| 132 | 132 |
|
| 133 | 133 |
} |
| 134 | 134 |
if flags.Changed(flagReserveCPU) || flags.Changed(flagReserveMemory) {
|
| ... | ... |
@@ -136,11 +136,11 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
| 136 | 136 |
task.Resources = &swarm.ResourceRequirements{}
|
| 137 | 137 |
} |
| 138 | 138 |
task.Resources.Reservations = &swarm.Resources{}
|
| 139 |
- mergeInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs) |
|
| 140 |
- mergeInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes) |
|
| 139 |
+ updateInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs) |
|
| 140 |
+ updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes) |
|
| 141 | 141 |
} |
| 142 | 142 |
|
| 143 |
- mergeDurationOpt("stop-grace-period", cspec.StopGracePeriod)
|
|
| 143 |
+ updateDurationOpt("stop-grace-period", cspec.StopGracePeriod)
|
|
| 144 | 144 |
|
| 145 | 145 |
if flags.Changed(flagRestartCondition) || flags.Changed(flagRestartDelay) || flags.Changed(flagRestartMaxAttempts) || flags.Changed(flagRestartWindow) {
|
| 146 | 146 |
if task.RestartPolicy == nil {
|
| ... | ... |
@@ -151,17 +151,17 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
| 151 | 151 |
value, _ := flags.GetString(flagRestartCondition) |
| 152 | 152 |
task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value) |
| 153 | 153 |
} |
| 154 |
- mergeDurationOpt(flagRestartDelay, task.RestartPolicy.Delay) |
|
| 155 |
- mergeUint64Opt(flagRestartMaxAttempts, task.RestartPolicy.MaxAttempts) |
|
| 156 |
- mergeDurationOpt((flagRestartWindow), task.RestartPolicy.Window) |
|
| 154 |
+ updateDurationOpt(flagRestartDelay, task.RestartPolicy.Delay) |
|
| 155 |
+ updateUint64Opt(flagRestartMaxAttempts, task.RestartPolicy.MaxAttempts) |
|
| 156 |
+ updateDurationOpt((flagRestartWindow), task.RestartPolicy.Window) |
|
| 157 | 157 |
} |
| 158 | 158 |
|
| 159 | 159 |
if flags.Changed(flagConstraint) {
|
| 160 | 160 |
task.Placement = &swarm.Placement{}
|
| 161 |
- mergeSlice(flagConstraint, &task.Placement.Constraints) |
|
| 161 |
+ updateSlice(flagConstraint, &task.Placement.Constraints) |
|
| 162 | 162 |
} |
| 163 | 163 |
|
| 164 |
- if err := mergeMode(flags, &spec.Mode); err != nil {
|
|
| 164 |
+ if err := updateMode(flags, &spec.Mode); err != nil {
|
|
| 165 | 165 |
return err |
| 166 | 166 |
} |
| 167 | 167 |
|
| ... | ... |
@@ -169,11 +169,11 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
| 169 | 169 |
if spec.UpdateConfig == nil {
|
| 170 | 170 |
spec.UpdateConfig = &swarm.UpdateConfig{}
|
| 171 | 171 |
} |
| 172 |
- mergeUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism) |
|
| 173 |
- mergeDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) |
|
| 172 |
+ updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism) |
|
| 173 |
+ updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) |
|
| 174 | 174 |
} |
| 175 | 175 |
|
| 176 |
- mergeNetworks(flags, &spec.Networks) |
|
| 176 |
+ updateNetworks(flags, &spec.Networks) |
|
| 177 | 177 |
if flags.Changed(flagEndpointMode) {
|
| 178 | 178 |
value, _ := flags.GetString(flagEndpointMode) |
| 179 | 179 |
spec.EndpointSpec.Mode = swarm.ResolutionMode(value) |
| ... | ... |
@@ -183,38 +183,36 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
| 183 | 183 |
if spec.EndpointSpec == nil {
|
| 184 | 184 |
spec.EndpointSpec = &swarm.EndpointSpec{}
|
| 185 | 185 |
} |
| 186 |
- mergePorts(flags, &spec.EndpointSpec.Ports) |
|
| 186 |
+ updatePorts(flags, &spec.EndpointSpec.Ports) |
|
| 187 | 187 |
} |
| 188 | 188 |
return nil |
| 189 | 189 |
} |
| 190 | 190 |
|
| 191 |
-func mergeLabels(flags *pflag.FlagSet, field *map[string]string) {
|
|
| 191 |
+func updateLabels(flags *pflag.FlagSet, field *map[string]string) {
|
|
| 192 | 192 |
if !flags.Changed(flagLabel) {
|
| 193 | 193 |
return |
| 194 | 194 |
} |
| 195 | 195 |
|
| 196 |
- if *field == nil {
|
|
| 197 |
- *field = make(map[string]string) |
|
| 198 |
- } |
|
| 199 |
- |
|
| 200 | 196 |
values := flags.Lookup(flagLabel).Value.(*opts.ListOpts).GetAll() |
| 197 |
+ |
|
| 198 |
+ localLabels := map[string]string{}
|
|
| 201 | 199 |
for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
|
| 202 |
- (*field)[key] = value |
|
| 200 |
+ localLabels[key] = value |
|
| 203 | 201 |
} |
| 202 |
+ *field = localLabels |
|
| 204 | 203 |
} |
| 205 | 204 |
|
| 206 | 205 |
// TODO: should this override by destination path, or does swarm handle that? |
| 207 |
-func mergeMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
|
|
| 206 |
+func updateMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
|
|
| 208 | 207 |
if !flags.Changed(flagMount) {
|
| 209 | 208 |
return |
| 210 | 209 |
} |
| 211 | 210 |
|
| 212 |
- values := flags.Lookup(flagMount).Value.(*MountOpt).Value() |
|
| 213 |
- *mounts = append(*mounts, values...) |
|
| 211 |
+ *mounts = flags.Lookup(flagMount).Value.(*MountOpt).Value() |
|
| 214 | 212 |
} |
| 215 | 213 |
|
| 216 | 214 |
// TODO: should this override by name, or does swarm handle that? |
| 217 |
-func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
|
|
| 215 |
+func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
|
|
| 218 | 216 |
if !flags.Changed(flagPublish) {
|
| 219 | 217 |
return |
| 220 | 218 |
} |
| ... | ... |
@@ -222,22 +220,27 @@ func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
|
| 222 | 222 |
values := flags.Lookup(flagPublish).Value.(*opts.ListOpts).GetAll() |
| 223 | 223 |
ports, portBindings, _ := nat.ParsePortSpecs(values) |
| 224 | 224 |
|
| 225 |
+ var localPortConfig []swarm.PortConfig |
|
| 225 | 226 |
for port := range ports {
|
| 226 |
- *portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...) |
|
| 227 |
+ localPortConfig = append(localPortConfig, convertPortToPortConfig(port, portBindings)...) |
|
| 227 | 228 |
} |
| 229 |
+ *portConfig = localPortConfig |
|
| 228 | 230 |
} |
| 229 | 231 |
|
| 230 |
-func mergeNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
|
|
| 232 |
+func updateNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
|
|
| 231 | 233 |
if !flags.Changed(flagNetwork) {
|
| 232 | 234 |
return |
| 233 | 235 |
} |
| 234 | 236 |
networks, _ := flags.GetStringSlice(flagNetwork) |
| 237 |
+ |
|
| 238 |
+ var localAttachments []swarm.NetworkAttachmentConfig |
|
| 235 | 239 |
for _, network := range networks {
|
| 236 |
- *attachments = append(*attachments, swarm.NetworkAttachmentConfig{Target: network})
|
|
| 240 |
+ localAttachments = append(localAttachments, swarm.NetworkAttachmentConfig{Target: network})
|
|
| 237 | 241 |
} |
| 242 |
+ *attachments = localAttachments |
|
| 238 | 243 |
} |
| 239 | 244 |
|
| 240 |
-func mergeMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
|
|
| 245 |
+func updateMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
|
|
| 241 | 246 |
if !flags.Changed(flagMode) && !flags.Changed(flagReplicas) {
|
| 242 | 247 |
return nil |
| 243 | 248 |
} |
| 244 | 249 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,39 @@ |
| 0 |
+// +build !windows |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "github.com/docker/docker/pkg/integration/checker" |
|
| 6 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 7 |
+ "github.com/go-check/check" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func setPortConfig(portConfig []swarm.PortConfig) serviceConstructor {
|
|
| 11 |
+ return func(s *swarm.Service) {
|
|
| 12 |
+ if s.Spec.EndpointSpec == nil {
|
|
| 13 |
+ s.Spec.EndpointSpec = &swarm.EndpointSpec{}
|
|
| 14 |
+ } |
|
| 15 |
+ s.Spec.EndpointSpec.Ports = portConfig |
|
| 16 |
+ } |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+func (s *DockerSwarmSuite) TestApiServiceUpdatePort(c *check.C) {
|
|
| 20 |
+ d := s.AddDaemon(c, true, true) |
|
| 21 |
+ |
|
| 22 |
+ // Create a service with a port mapping of 8080:8081. |
|
| 23 |
+ portConfig := []swarm.PortConfig{{TargetPort: 8081, PublishedPort: 8080}}
|
|
| 24 |
+ serviceID := d.createService(c, simpleTestService, setInstances(1), setPortConfig(portConfig)) |
|
| 25 |
+ waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) |
|
| 26 |
+ |
|
| 27 |
+ // Update the service: changed the port mapping from 8080:8081 to 8082:8083. |
|
| 28 |
+ updatedPortConfig := []swarm.PortConfig{{TargetPort: 8083, PublishedPort: 8082}}
|
|
| 29 |
+ remoteService := d.getService(c, serviceID) |
|
| 30 |
+ d.updateService(c, remoteService, setPortConfig(updatedPortConfig)) |
|
| 31 |
+ |
|
| 32 |
+ // Inspect the service and verify port mapping. |
|
| 33 |
+ updatedService := d.getService(c, serviceID) |
|
| 34 |
+ c.Assert(updatedService.Spec.EndpointSpec, check.NotNil) |
|
| 35 |
+ c.Assert(len(updatedService.Spec.EndpointSpec.Ports), check.Equals, 1) |
|
| 36 |
+ c.Assert(updatedService.Spec.EndpointSpec.Ports[0].TargetPort, check.Equals, uint32(8083)) |
|
| 37 |
+ c.Assert(updatedService.Spec.EndpointSpec.Ports[0].PublishedPort, check.Equals, uint32(8082)) |
|
| 38 |
+} |
| 0 | 39 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,45 @@ |
| 0 |
+// +build !windows |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/pkg/integration/checker" |
|
| 8 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 9 |
+ "github.com/go-check/check" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func (s *DockerSwarmSuite) TestServiceUpdatePort(c *check.C) {
|
|
| 13 |
+ d := s.AddDaemon(c, true, true) |
|
| 14 |
+ |
|
| 15 |
+ serviceName := "TestServiceUpdatePort" |
|
| 16 |
+ serviceArgs := append([]string{"create", "--name", serviceName, "-p", "8080:8081", defaultSleepImage}, defaultSleepCommand...)
|
|
| 17 |
+ |
|
| 18 |
+ // Create a service with a port mapping of 8080:8081. |
|
| 19 |
+ out, err := d.Cmd("service", serviceArgs...)
|
|
| 20 |
+ c.Assert(err, checker.IsNil) |
|
| 21 |
+ waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) |
|
| 22 |
+ |
|
| 23 |
+ // Update the service: changed the port mapping from 8080:8081 to 8082:8083. |
|
| 24 |
+ _, err = d.Cmd("service", "update", "-p", "8082:8083", serviceName)
|
|
| 25 |
+ c.Assert(err, checker.IsNil) |
|
| 26 |
+ |
|
| 27 |
+ // Inspect the service and verify port mapping |
|
| 28 |
+ expected := []swarm.PortConfig{
|
|
| 29 |
+ {
|
|
| 30 |
+ Protocol: "tcp", |
|
| 31 |
+ PublishedPort: 8082, |
|
| 32 |
+ TargetPort: 8083, |
|
| 33 |
+ }, |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.EndpointSpec.Ports }}", serviceName)
|
|
| 37 |
+ c.Assert(err, checker.IsNil) |
|
| 38 |
+ |
|
| 39 |
+ var portConfig []swarm.PortConfig |
|
| 40 |
+ if err := json.Unmarshal([]byte(out), &portConfig); err != nil {
|
|
| 41 |
+ c.Fatalf("invalid JSON in inspect result: %v (%s)", err, out)
|
|
| 42 |
+ } |
|
| 43 |
+ c.Assert(portConfig, checker.DeepEquals, expected) |
|
| 44 |
+} |