mirror of https://github.com/docker/cli.git
Fix --network-add adding duplicate networks
When adding a network using `docker service update --network-add`, the new network was added by _name_. Existing entries in a service spec are listed by network ID, which resulted in the CLI not detecting duplicate entries for the same network. This patch changes the behavior to always use the network-ID, so that duplicate entries are correctly caught. Before this change; $ docker network create -d overlay foo $ docker service create --name=test --network=foo nginx:alpine $ docker service update --network-add foo test $ docker service inspect --format '{{ json .Spec.TaskTemplate.Networks}}' test [ { "Target": "9ot0ieagg5xv1gxd85m7y33eq" }, { "Target": "9ot0ieagg5xv1gxd85m7y33eq" } ] After this change: $ docker network create -d overlay foo $ docker service create --name=test --network=foo nginx:alpine $ docker service update --network-add foo test service is already attached to network foo Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
16cccc30f9
commit
e6ebaf55dd
|
@ -16,6 +16,7 @@ type fakeClient struct {
|
||||||
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
|
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
|
||||||
taskListFunc func(context.Context, types.TaskListOptions) ([]swarm.Task, error)
|
taskListFunc func(context.Context, types.TaskListOptions) ([]swarm.Task, error)
|
||||||
infoFunc func(ctx context.Context) (types.Info, error)
|
infoFunc func(ctx context.Context) (types.Info, error)
|
||||||
|
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||||
|
@ -60,6 +61,13 @@ func (f *fakeClient) Info(ctx context.Context) (types.Info, error) {
|
||||||
return f.infoFunc(ctx)
|
return f.infoFunc(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeClient) NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||||
|
if f.networkInspectFunc != nil {
|
||||||
|
return f.networkInspectFunc(ctx, networkID, options)
|
||||||
|
}
|
||||||
|
return types.NetworkResource{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func newService(id string, name string) swarm.Service {
|
func newService(id string, name string) swarm.Service {
|
||||||
return swarm.Service{
|
return swarm.Service{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
|
|
@ -353,22 +353,21 @@ func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
|
||||||
return c.value
|
return c.value
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks opts.NetworkOpt) ([]swarm.NetworkAttachmentConfig, error) {
|
func resolveNetworkID(ctx context.Context, apiClient client.NetworkAPIClient, networkIDOrName string) (string, error) {
|
||||||
|
nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
||||||
|
return nw.ID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig {
|
||||||
var netAttach []swarm.NetworkAttachmentConfig
|
var netAttach []swarm.NetworkAttachmentConfig
|
||||||
for _, net := range networks.Value() {
|
for _, net := range networks.Value() {
|
||||||
networkIDOrName := net.Target
|
|
||||||
_, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
netAttach = append(netAttach, swarm.NetworkAttachmentConfig{
|
netAttach = append(netAttach, swarm.NetworkAttachmentConfig{
|
||||||
Target: net.Target,
|
Target: net.Target,
|
||||||
Aliases: net.Aliases,
|
Aliases: net.Aliases,
|
||||||
DriverOpts: net.DriverOpts,
|
DriverOpts: net.DriverOpts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Sort(byNetworkTarget(netAttach))
|
return netAttach
|
||||||
return netAttach, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type endpointOptions struct {
|
type endpointOptions struct {
|
||||||
|
@ -590,10 +589,15 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
||||||
return service, err
|
return service, err
|
||||||
}
|
}
|
||||||
|
|
||||||
networks, err := convertNetworks(ctx, apiClient, options.networks)
|
networks := convertNetworks(options.networks)
|
||||||
|
for i, net := range networks {
|
||||||
|
nwID, err := resolveNetworkID(ctx, apiClient, net.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return service, err
|
return service, err
|
||||||
}
|
}
|
||||||
|
networks[i].Target = nwID
|
||||||
|
}
|
||||||
|
sort.Sort(byNetworkTarget(networks))
|
||||||
|
|
||||||
resources, err := options.resources.ToResourceRequirements()
|
resources, err := options.resources.ToResourceRequirements()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMemBytesString(t *testing.T) {
|
func TestMemBytesString(t *testing.T) {
|
||||||
|
@ -123,3 +128,37 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToServiceNetwork(t *testing.T) {
|
||||||
|
nws := []types.NetworkResource{
|
||||||
|
{Name: "aaa-network", ID: "id555"},
|
||||||
|
{Name: "mmm-network", ID: "id999"},
|
||||||
|
{Name: "zzz-network", ID: "id111"},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &fakeClient{
|
||||||
|
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||||
|
for _, network := range nws {
|
||||||
|
if network.ID == networkID || network.Name == networkID {
|
||||||
|
return network, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
nwo := opts.NetworkOpt{}
|
||||||
|
nwo.Set("zzz-network")
|
||||||
|
nwo.Set("mmm-network")
|
||||||
|
nwo.Set("aaa-network")
|
||||||
|
|
||||||
|
o := newServiceOptions()
|
||||||
|
o.mode = "replicated"
|
||||||
|
o.networks = nwo
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
flags := newCreateCommand(nil).Flags()
|
||||||
|
service, err := o.ToService(ctx, client, flags)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id111"}, {Target: "id555"}, {Target: "id999"}}, service.TaskTemplate.Networks)
|
||||||
|
}
|
||||||
|
|
|
@ -1119,14 +1119,16 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
|
||||||
|
|
||||||
if flags.Changed(flagNetworkAdd) {
|
if flags.Changed(flagNetworkAdd) {
|
||||||
values := flags.Lookup(flagNetworkAdd).Value.(*opts.NetworkOpt)
|
values := flags.Lookup(flagNetworkAdd).Value.(*opts.NetworkOpt)
|
||||||
networks, err := convertNetworks(ctx, apiClient, *values)
|
networks := convertNetworks(*values)
|
||||||
|
for _, network := range networks {
|
||||||
|
nwID, err := resolveNetworkID(ctx, apiClient, network.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, network := range networks {
|
if _, exists := existingNetworks[nwID]; exists {
|
||||||
if _, exists := existingNetworks[network.Target]; exists {
|
|
||||||
return errors.Errorf("service is already attached to network %s", network.Target)
|
return errors.Errorf("service is already attached to network %s", network.Target)
|
||||||
}
|
}
|
||||||
|
network.Target = nwID
|
||||||
newNetworks = append(newNetworks, network)
|
newNetworks = append(newNetworks, network)
|
||||||
existingNetworks[network.Target] = struct{}{}
|
existingNetworks[network.Target] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -586,3 +587,69 @@ func TestRemoveGenericResources(t *testing.T) {
|
||||||
assert.NoError(t, removeGenericResources(flags, task))
|
assert.NoError(t, removeGenericResources(flags, task))
|
||||||
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
|
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateNetworks(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
nws := []types.NetworkResource{
|
||||||
|
{Name: "aaa-network", ID: "id555"},
|
||||||
|
{Name: "mmm-network", ID: "id999"},
|
||||||
|
{Name: "zzz-network", ID: "id111"},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &fakeClient{
|
||||||
|
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||||
|
for _, network := range nws {
|
||||||
|
if network.ID == networkID || network.Name == networkID {
|
||||||
|
return network, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := swarm.ServiceSpec{
|
||||||
|
TaskTemplate: swarm.TaskSpec{
|
||||||
|
ContainerSpec: &swarm.ContainerSpec{},
|
||||||
|
Networks: []swarm.NetworkAttachmentConfig{
|
||||||
|
{Target: "id999"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := newUpdateCommand(nil).Flags()
|
||||||
|
err := flags.Set(flagNetworkAdd, "aaa-network")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = updateService(ctx, client, flags, &svc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||||
|
|
||||||
|
flags = newUpdateCommand(nil).Flags()
|
||||||
|
err = flags.Set(flagNetworkAdd, "aaa-network")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = updateService(ctx, client, flags, &svc)
|
||||||
|
assert.EqualError(t, err, "service is already attached to network aaa-network")
|
||||||
|
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||||
|
|
||||||
|
flags = newUpdateCommand(nil).Flags()
|
||||||
|
err = flags.Set(flagNetworkAdd, "id555")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = updateService(ctx, client, flags, &svc)
|
||||||
|
assert.EqualError(t, err, "service is already attached to network id555")
|
||||||
|
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||||
|
|
||||||
|
flags = newUpdateCommand(nil).Flags()
|
||||||
|
err = flags.Set(flagNetworkRemove, "id999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = updateService(ctx, client, flags, &svc)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks)
|
||||||
|
|
||||||
|
flags = newUpdateCommand(nil).Flags()
|
||||||
|
err = flags.Set(flagNetworkAdd, "mmm-network")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = flags.Set(flagNetworkRemove, "aaa-network")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = updateService(ctx, client, flags, &svc)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue