cli: Allow service's networks to be updated

Resolve networks IDs on the client side.

Avoid filling in deprecated Spec.Networks field.

Sort networks in the TaskSpec for update stability.

Add an integration test for changing service networks.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This commit is contained in:
Aaron Lehmann 2017-03-23 17:51:57 -07:00
parent 091b5e68ea
commit 0f2669a638
7 changed files with 168 additions and 21 deletions

View file

@ -63,7 +63,9 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
apiClient := dockerCli.Client() apiClient := dockerCli.Client()
createOpts := types.ServiceCreateOptions{} createOpts := types.ServiceCreateOptions{}
service, err := opts.ToService() ctx := context.Background()
service, err := opts.ToService(ctx, apiClient)
if err != nil { if err != nil {
return err return err
} }
@ -79,8 +81,6 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
} }
ctx := context.Background()
if err := resolveServiceImageDigest(dockerCli, &service); err != nil { if err := resolveServiceImageDigest(dockerCli, &service); err != nil {
return err return err
} }

View file

@ -2,17 +2,20 @@ package service
import ( import (
"fmt" "fmt"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/opts" "github.com/docker/docker/opts"
runconfigopts "github.com/docker/docker/runconfig/opts" runconfigopts "github.com/docker/docker/runconfig/opts"
shlex "github.com/flynn-archive/go-shlex" shlex "github.com/flynn-archive/go-shlex"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"golang.org/x/net/context"
) )
type int64Value interface { type int64Value interface {
@ -270,12 +273,17 @@ func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
return c.value return c.value
} }
func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks []string) ([]swarm.NetworkAttachmentConfig, error) {
nets := []swarm.NetworkAttachmentConfig{} nets := []swarm.NetworkAttachmentConfig{}
for _, network := range networks { for _, networkIDOrName := range networks {
nets = append(nets, swarm.NetworkAttachmentConfig{Target: network}) network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false)
if err != nil {
return nil, err
}
nets = append(nets, swarm.NetworkAttachmentConfig{Target: network.ID})
} }
return nets sort.Sort(byNetworkTarget(nets))
return nets, nil
} }
type endpointOptions struct { type endpointOptions struct {
@ -455,7 +463,7 @@ func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) {
return serviceMode, nil return serviceMode, nil
} }
func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.APIClient) (swarm.ServiceSpec, error) {
var service swarm.ServiceSpec var service swarm.ServiceSpec
envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll()) envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll())
@ -487,6 +495,11 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
return service, err return service, err
} }
networks, err := convertNetworks(ctx, apiClient, opts.networks.GetAll())
if err != nil {
return service, err
}
service = swarm.ServiceSpec{ service = swarm.ServiceSpec{
Annotations: swarm.Annotations{ Annotations: swarm.Annotations{
Name: opts.name, Name: opts.name,
@ -517,7 +530,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
Secrets: nil, Secrets: nil,
Healthcheck: healthConfig, Healthcheck: healthConfig,
}, },
Networks: convertNetworks(opts.networks.GetAll()), Networks: networks,
Resources: opts.resources.ToResourceRequirements(), Resources: opts.resources.ToResourceRequirements(),
RestartPolicy: opts.restartPolicy.ToRestartPolicy(), RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
Placement: &swarm.Placement{ Placement: &swarm.Placement{
@ -526,7 +539,6 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
}, },
LogDriver: opts.logDriver.toLogDriver(), LogDriver: opts.logDriver.toLogDriver(),
}, },
Networks: convertNetworks(opts.networks.GetAll()),
Mode: serviceMode, Mode: serviceMode,
UpdateConfig: opts.update.config(), UpdateConfig: opts.update.config(),
RollbackConfig: opts.rollback.config(), RollbackConfig: opts.rollback.config(),
@ -666,6 +678,8 @@ const (
flagMountAdd = "mount-add" flagMountAdd = "mount-add"
flagName = "name" flagName = "name"
flagNetwork = "network" flagNetwork = "network"
flagNetworkAdd = "network-add"
flagNetworkRemove = "network-rm"
flagPublish = "publish" flagPublish = "publish"
flagPublishRemove = "publish-rm" flagPublishRemove = "publish-rm"
flagPublishAdd = "publish-add" flagPublishAdd = "publish-add"

View file

@ -74,6 +74,10 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"}) flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"})
flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference") flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference")
flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"}) flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"})
flags.Var(&serviceOpts.networks, flagNetworkAdd, "Add a network")
flags.SetAnnotation(flagNetworkAdd, "version", []string{"1.29"})
flags.Var(newListOptsVar(), flagNetworkRemove, "Remove a network")
flags.SetAnnotation(flagNetworkRemove, "version", []string{"1.29"})
flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port")
flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container")
flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"})
@ -147,7 +151,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
updateOpts.Rollback = "previous" updateOpts.Rollback = "previous"
} }
err = updateService(flags, spec) err = updateService(ctx, apiClient, flags, spec)
if err != nil { if err != nil {
return err return err
} }
@ -207,7 +211,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
return waitOnService(ctx, dockerCli, serviceID, opts) return waitOnService(ctx, dockerCli, serviceID, opts)
} }
func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { func updateService(ctx context.Context, apiClient client.APIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
updateString := func(flag string, field *string) { updateString := func(flag string, field *string) {
if flags.Changed(flag) { if flags.Changed(flag) {
*field, _ = flags.GetString(flag) *field, _ = flags.GetString(flag)
@ -316,6 +320,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
updatePlacementPreferences(flags, task.Placement) updatePlacementPreferences(flags, task.Placement)
} }
if anyChanged(flags, flagNetworkAdd, flagNetworkRemove) {
if err := updateNetworks(ctx, apiClient, flags, spec); err != nil {
return err
}
}
if err := updateReplicas(flags, &spec.Mode); err != nil { if err := updateReplicas(flags, &spec.Mode); err != nil {
return err return err
} }
@ -623,7 +633,6 @@ func (m byMountSource) Less(i, j int) bool {
} }
func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error { func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error {
mountsByTarget := map[string]mounttypes.Mount{} mountsByTarget := map[string]mounttypes.Mount{}
if flags.Changed(flagMountAdd) { if flags.Changed(flagMountAdd) {
@ -947,3 +956,63 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec)
} }
return nil return nil
} }
type byNetworkTarget []swarm.NetworkAttachmentConfig
func (m byNetworkTarget) Len() int { return len(m) }
func (m byNetworkTarget) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m byNetworkTarget) Less(i, j int) bool {
return m[i].Target < m[j].Target
}
func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
// spec.TaskTemplate.Networks takes precedence over the deprecated
// spec.Networks field. If spec.Network is in use, we'll migrate those
// values to spec.TaskTemplate.Networks.
specNetworks := spec.TaskTemplate.Networks
if len(specNetworks) == 0 {
specNetworks = spec.Networks
}
spec.Networks = nil
toRemove := buildToRemoveSet(flags, flagNetworkRemove)
idsToRemove := make(map[string]struct{})
for networkIDOrName := range toRemove {
network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false)
if err != nil {
return err
}
idsToRemove[network.ID] = struct{}{}
}
existingNetworks := make(map[string]struct{})
var newNetworks []swarm.NetworkAttachmentConfig
for _, network := range specNetworks {
if _, exists := idsToRemove[network.Target]; exists {
continue
}
newNetworks = append(newNetworks, network)
existingNetworks[network.Target] = struct{}{}
}
if flags.Changed(flagNetworkAdd) {
values := flags.Lookup(flagNetworkAdd).Value.(*opts.ListOpts).GetAll()
networks, err := convertNetworks(ctx, apiClient, values)
if err != nil {
return err
}
for _, network := range networks {
if _, exists := existingNetworks[network.Target]; exists {
return errors.Errorf("service is already attached to network %s", network.Target)
}
newNetworks = append(newNetworks, network)
existingNetworks[network.Target] = struct{}{}
}
}
sort.Sort(byNetworkTarget(newNetworks))
spec.TaskTemplate.Networks = newNetworks
return nil
}

View file

@ -22,7 +22,7 @@ func TestUpdateServiceArgs(t *testing.T) {
cspec := &spec.TaskTemplate.ContainerSpec cspec := &spec.TaskTemplate.ContainerSpec
cspec.Args = []string{"old", "args"} cspec.Args = []string{"old", "args"}
updateService(flags, spec) updateService(nil, nil, flags, spec)
assert.EqualStringSlice(t, cspec.Args, []string{"the", "new args"}) assert.EqualStringSlice(t, cspec.Args, []string{"the", "new args"})
} }
@ -458,18 +458,18 @@ func TestUpdateReadOnly(t *testing.T) {
// Update with --read-only=true, changed to true // Update with --read-only=true, changed to true
flags := newUpdateCommand(nil).Flags() flags := newUpdateCommand(nil).Flags()
flags.Set("read-only", "true") flags.Set("read-only", "true")
updateService(flags, spec) updateService(nil, nil, flags, spec)
assert.Equal(t, cspec.ReadOnly, true) assert.Equal(t, cspec.ReadOnly, true)
// Update without --read-only, no change // Update without --read-only, no change
flags = newUpdateCommand(nil).Flags() flags = newUpdateCommand(nil).Flags()
updateService(flags, spec) updateService(nil, nil, flags, spec)
assert.Equal(t, cspec.ReadOnly, true) assert.Equal(t, cspec.ReadOnly, true)
// Update with --read-only=false, changed to false // Update with --read-only=false, changed to false
flags = newUpdateCommand(nil).Flags() flags = newUpdateCommand(nil).Flags()
flags.Set("read-only", "false") flags.Set("read-only", "false")
updateService(flags, spec) updateService(nil, nil, flags, spec)
assert.Equal(t, cspec.ReadOnly, false) assert.Equal(t, cspec.ReadOnly, false)
} }
@ -480,17 +480,17 @@ func TestUpdateStopSignal(t *testing.T) {
// Update with --stop-signal=SIGUSR1 // Update with --stop-signal=SIGUSR1
flags := newUpdateCommand(nil).Flags() flags := newUpdateCommand(nil).Flags()
flags.Set("stop-signal", "SIGUSR1") flags.Set("stop-signal", "SIGUSR1")
updateService(flags, spec) updateService(nil, nil, flags, spec)
assert.Equal(t, cspec.StopSignal, "SIGUSR1") assert.Equal(t, cspec.StopSignal, "SIGUSR1")
// Update without --stop-signal, no change // Update without --stop-signal, no change
flags = newUpdateCommand(nil).Flags() flags = newUpdateCommand(nil).Flags()
updateService(flags, spec) updateService(nil, nil, flags, spec)
assert.Equal(t, cspec.StopSignal, "SIGUSR1") assert.Equal(t, cspec.StopSignal, "SIGUSR1")
// Update with --stop-signal=SIGWINCH // Update with --stop-signal=SIGWINCH
flags = newUpdateCommand(nil).Flags() flags = newUpdateCommand(nil).Flags()
flags.Set("stop-signal", "SIGWINCH") flags.Set("stop-signal", "SIGWINCH")
updateService(flags, spec) updateService(nil, nil, flags, spec)
assert.Equal(t, cspec.StopSignal, "SIGWINCH") assert.Equal(t, cspec.StopSignal, "SIGWINCH")
} }

View file

@ -58,6 +58,8 @@ Options:
--log-opt list Logging driver options (default []) --log-opt list Logging driver options (default [])
--mount-add mount Add or update a mount on a service --mount-add mount Add or update a mount on a service
--mount-rm list Remove a mount by its target path (default []) --mount-rm list Remove a mount by its target path (default [])
--network-add list Add a network
--network-rm list Remove a network
--no-healthcheck Disable any container-specified HEALTHCHECK --no-healthcheck Disable any container-specified HEALTHCHECK
--placement-pref-add pref Add a placement preference --placement-pref-add pref Add a placement preference
--placement-pref-rm pref Remove a placement preference --placement-pref-rm pref Remove a placement preference

View file

@ -205,7 +205,30 @@ func (d *Swarm) CheckServiceTasks(service string) func(*check.C) (interface{}, c
} }
} }
// CheckRunningTaskImages returns the number of different images attached to a running task // CheckRunningTaskNetworks returns the number of times each network is referenced from a task.
func (d *Swarm) CheckRunningTaskNetworks(c *check.C) (interface{}, check.CommentInterface) {
var tasks []swarm.Task
filterArgs := filters.NewArgs()
filterArgs.Add("desired-state", "running")
filters, err := filters.ToParam(filterArgs)
c.Assert(err, checker.IsNil)
status, out, err := d.SockRequest("GET", "/tasks?filters="+filters, nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
c.Assert(json.Unmarshal(out, &tasks), checker.IsNil)
result := make(map[string]int)
for _, task := range tasks {
for _, network := range task.Spec.Networks {
result[network.Target]++
}
}
return result, nil
}
// CheckRunningTaskImages returns the times each image is running as a task.
func (d *Swarm) CheckRunningTaskImages(c *check.C) (interface{}, check.CommentInterface) { func (d *Swarm) CheckRunningTaskImages(c *check.C) (interface{}, check.CommentInterface) {
var tasks []swarm.Task var tasks []swarm.Task

View file

@ -855,6 +855,45 @@ func (s *DockerSwarmSuite) TestSwarmServiceTTYUpdate(c *check.C) {
c.Assert(strings.TrimSpace(out), checker.Equals, "true") c.Assert(strings.TrimSpace(out), checker.Equals, "true")
} }
func (s *DockerSwarmSuite) TestSwarmServiceNetworkUpdate(c *check.C) {
d := s.AddDaemon(c, true, true)
result := icmd.RunCmd(d.Command("network", "create", "-d", "overlay", "foo"))
result.Assert(c, icmd.Success)
fooNetwork := strings.TrimSpace(string(result.Combined()))
result = icmd.RunCmd(d.Command("network", "create", "-d", "overlay", "bar"))
result.Assert(c, icmd.Success)
barNetwork := strings.TrimSpace(string(result.Combined()))
result = icmd.RunCmd(d.Command("network", "create", "-d", "overlay", "baz"))
result.Assert(c, icmd.Success)
bazNetwork := strings.TrimSpace(string(result.Combined()))
// Create a service
name := "top"
result = icmd.RunCmd(d.Command("service", "create", "--network", "foo", "--network", "bar", "--name", name, "busybox", "top"))
result.Assert(c, icmd.Success)
// Make sure task has been deployed.
waitAndAssert(c, defaultReconciliationTimeout, d.CheckRunningTaskNetworks, checker.DeepEquals,
map[string]int{fooNetwork: 1, barNetwork: 1})
// Remove a network
result = icmd.RunCmd(d.Command("service", "update", "--network-rm", "foo", name))
result.Assert(c, icmd.Success)
waitAndAssert(c, defaultReconciliationTimeout, d.CheckRunningTaskNetworks, checker.DeepEquals,
map[string]int{barNetwork: 1})
// Add a network
result = icmd.RunCmd(d.Command("service", "update", "--network-add", "baz", name))
result.Assert(c, icmd.Success)
waitAndAssert(c, defaultReconciliationTimeout, d.CheckRunningTaskNetworks, checker.DeepEquals,
map[string]int{barNetwork: 1, bazNetwork: 1})
}
func (s *DockerSwarmSuite) TestDNSConfig(c *check.C) { func (s *DockerSwarmSuite) TestDNSConfig(c *check.C) {
d := s.AddDaemon(c, true, true) d := s.AddDaemon(c, true, true)