diff --git a/libnetwork/controller.go b/libnetwork/controller.go index 8b2a983d51..84c3ee3d37 100644 --- a/libnetwork/controller.go +++ b/libnetwork/controller.go @@ -722,8 +722,27 @@ func (c *controller) NewNetwork(networkType, name string, id string, options ... } network.processOptions(options...) + if err := network.validateConfiguration(); err != nil { + return nil, err + } - _, cap, err := network.resolveDriver(networkType, true) + var ( + cap *driverapi.Capability + err error + ) + + // Reset network types, force local scope and skip allocation and + // plumbing for configuration networks. Reset of the config-only + // network drivers is needed so that this special network is not + // usable by old engine versions. + if network.configOnly { + network.scope = datastore.LocalScope + network.networkType = "null" + network.ipamType = "" + goto addToStore + } + + _, cap, err = network.resolveDriver(network.networkType, true) if err != nil { return nil, err } @@ -737,7 +756,6 @@ func (c *controller) NewNetwork(networkType, name string, id string, options ... // For non-distributed controlled environment, globalscoped non-dynamic networks are redirected to Manager return nil, ManagerRedirectError(name) } - return nil, types.ForbiddenErrorf("Cannot create a multi-host network from a worker node. Please create the network from a manager node.") } @@ -747,6 +765,26 @@ func (c *controller) NewNetwork(networkType, name string, id string, options ... return nil, err } + // From this point on, we need the network specific configuration, + // which may come from a configuration-only network + if network.configFrom != "" { + t, err := c.getConfigNetwork(network.configFrom) + if err != nil { + return nil, types.NotFoundErrorf("configuration network %q does not exist", network.configFrom) + } + if err := t.applyConfigurationTo(network); err != nil { + return nil, types.InternalErrorf("Failed to apply configuration: %v", err) + } + defer func() { + if err == nil { + if err := t.getEpCnt().IncEndpointCnt(); err != nil { + logrus.Warnf("Failed to update reference count for configuration network %q on creation of network %q: %v", + t.Name(), network.Name(), err) + } + } + }() + } + err = network.ipamAllocate() if err != nil { return nil, err @@ -769,6 +807,7 @@ func (c *controller) NewNetwork(networkType, name string, id string, options ... } }() +addToStore: // First store the endpoint count, then the network. To avoid to // end up with a datastore containing a network and not an epCnt, // in case of an ungraceful shutdown during this function call. @@ -788,6 +827,9 @@ func (c *controller) NewNetwork(networkType, name string, id string, options ... if err = c.updateToStore(network); err != nil { return nil, err } + if network.configOnly { + return network, nil + } joinCluster(network) if !c.isDistributedControl() { @@ -801,6 +843,9 @@ func (c *controller) NewNetwork(networkType, name string, id string, options ... var joinCluster NetworkWalker = func(nw Network) bool { n := nw.(*network) + if n.configOnly { + return false + } if err := n.joinCluster(); err != nil { logrus.Errorf("Failed to join network %s (%s) into agent cluster: %v", n.Name(), n.ID(), err) } @@ -816,6 +861,9 @@ func (c *controller) reservePools() { } for _, n := range networks { + if n.configOnly { + continue + } if !doReplayPoolReserve(n) { continue } diff --git a/libnetwork/endpoint.go b/libnetwork/endpoint.go index 8e70c38d76..e607eddaf2 100644 --- a/libnetwork/endpoint.go +++ b/libnetwork/endpoint.go @@ -1152,6 +1152,9 @@ func (c *controller) cleanupLocalEndpoints() { } for _, n := range nl { + if n.ConfigOnly() { + continue + } epl, err := n.getEndpointsFromStore() if err != nil { logrus.Warnf("Could not get list of endpoints in network %s during endpoint cleanup: %v", n.name, err) diff --git a/libnetwork/libnetwork_internal_test.go b/libnetwork/libnetwork_internal_test.go index bfd026d1e2..927d623daf 100644 --- a/libnetwork/libnetwork_internal_test.go +++ b/libnetwork/libnetwork_internal_test.go @@ -25,6 +25,8 @@ func TestNetworkMarshalling(t *testing.T) { networkType: "bridge", enableIPv6: true, persist: true, + configOnly: true, + configFrom: "configOnlyX", ipamOptions: map[string]string{ netlabel.MacAddress: "a:b:c:d:e:f", "primary": "", @@ -136,7 +138,8 @@ func TestNetworkMarshalling(t *testing.T) { !compareIpamInfoList(n.ipamV6Info, nn.ipamV6Info) || !compareStringMaps(n.ipamOptions, nn.ipamOptions) || !compareStringMaps(n.labels, nn.labels) || - !n.created.Equal(nn.created) { + !n.created.Equal(nn.created) || + n.configOnly != nn.configOnly || n.configFrom != nn.configFrom { t.Fatalf("JSON marsh/unmarsh failed."+ "\nOriginal:\n%#v\nDecoded:\n%#v"+ "\nOriginal ipamV4Conf: %#v\n\nDecoded ipamV4Conf: %#v"+ diff --git a/libnetwork/libnetwork_test.go b/libnetwork/libnetwork_test.go index 22c1b6fc1e..c9c645f114 100644 --- a/libnetwork/libnetwork_test.go +++ b/libnetwork/libnetwork_test.go @@ -359,6 +359,109 @@ func TestDeleteNetworkWithActiveEndpoints(t *testing.T) { } } +func TestNetworkConfig(t *testing.T) { + if !testutils.IsRunningInContainer() { + defer testutils.SetupTestOSContext(t)() + } + + // Verify config network cannot inherit another config network + configNetwork, err := controller.NewNetwork("bridge", "config_network0", "", + libnetwork.NetworkOptionConfigOnly(), + libnetwork.NetworkOptionConfigFrom("anotherConfigNw")) + + if err == nil { + t.Fatal("Expected to fail. But instead succeeded") + } + if _, ok := err.(types.ForbiddenError); !ok { + t.Fatalf("Did not fail with expected error. Actual error: %v", err) + } + + // Create supported config network + netOption := options.Generic{ + "EnableICC": false, + } + option := options.Generic{ + netlabel.GenericData: netOption, + } + ipamV4ConfList := []*libnetwork.IpamConf{{PreferredPool: "192.168.100.0/24", SubPool: "192.168.100.128/25", Gateway: "192.168.100.1"}} + ipamV6ConfList := []*libnetwork.IpamConf{{PreferredPool: "2001:db8:abcd::/64", SubPool: "2001:db8:abcd::ef99/80", Gateway: "2001:db8:abcd::22"}} + + netOptions := []libnetwork.NetworkOption{ + libnetwork.NetworkOptionConfigOnly(), + libnetwork.NetworkOptionEnableIPv6(true), + libnetwork.NetworkOptionGeneric(option), + libnetwork.NetworkOptionIpam("default", "", ipamV4ConfList, ipamV6ConfList, nil), + } + + configNetwork, err = controller.NewNetwork(bridgeNetType, "config_network0", "", netOptions...) + if err != nil { + t.Fatal(err) + } + + // Verify a config-only network cannot be created with network operator configurations + for i, opt := range []libnetwork.NetworkOption{ + libnetwork.NetworkOptionInternalNetwork(), + libnetwork.NetworkOptionAttachable(true), + libnetwork.NetworkOptionIngress(true), + } { + _, err = controller.NewNetwork(bridgeNetType, "testBR", "", + libnetwork.NetworkOptionConfigOnly(), opt) + if err == nil { + t.Fatalf("Expected to fail. But instead succeeded for option: %d", i) + } + if _, ok := err.(types.ForbiddenError); !ok { + t.Fatalf("Did not fail with expected error. Actual error: %v", err) + } + } + + // Verify a network cannot be created with both config-from and network specific configurations + for i, opt := range []libnetwork.NetworkOption{ + libnetwork.NetworkOptionEnableIPv6(true), + libnetwork.NetworkOptionIpam("my-ipam", "", nil, nil, nil), + libnetwork.NetworkOptionIpam("", "", ipamV4ConfList, nil, nil), + libnetwork.NetworkOptionIpam("", "", nil, ipamV6ConfList, nil), + libnetwork.NetworkOptionLabels(map[string]string{"number": "two"}), + libnetwork.NetworkOptionDriverOpts(map[string]string{"com.docker.network.mtu": "1600"}), + } { + _, err = controller.NewNetwork(bridgeNetType, "testBR", "", + libnetwork.NetworkOptionConfigFrom("config_network0"), opt) + if err == nil { + t.Fatalf("Expected to fail. But instead succeeded for option: %d", i) + } + if _, ok := err.(types.ForbiddenError); !ok { + t.Fatalf("Did not fail with expected error. Actual error: %v", err) + } + } + + // Create a valid network + network, err := controller.NewNetwork(bridgeNetType, "testBR", "", + libnetwork.NetworkOptionConfigFrom("config_network0")) + if err != nil { + t.Fatal(err) + } + + // Verify the config network cannot be removed + err = configNetwork.Delete() + if err == nil { + t.Fatal("Expected to fail. But instead succeeded") + } + + if _, ok := err.(types.ForbiddenError); !ok { + t.Fatalf("Did not fail with expected error. Actual error: %v", err) + } + + // Delete network + if err := network.Delete(); err != nil { + t.Fatal(err) + } + + // Verify the config network can now be removed + if err := configNetwork.Delete(); err != nil { + t.Fatal(err) + } + +} + func TestUnknownNetwork(t *testing.T) { if !testutils.IsRunningInContainer() { defer testutils.SetupTestOSContext(t)() diff --git a/libnetwork/network.go b/libnetwork/network.go index 8077770018..7be7b0af9b 100644 --- a/libnetwork/network.go +++ b/libnetwork/network.go @@ -67,6 +67,8 @@ type NetworkInfo interface { Internal() bool Attachable() bool Ingress() bool + ConfigFrom() string + ConfigOnly() bool Labels() map[string]string Dynamic() bool Created() time.Time @@ -219,6 +221,8 @@ type network struct { ingress bool driverTables []networkDBTable dynamic bool + configOnly bool + configFrom string sync.Mutex } @@ -348,6 +352,95 @@ func (i *IpamInfo) CopyTo(dstI *IpamInfo) error { return nil } +func (n *network) validateConfiguration() error { + if n.configOnly { + // Only supports network specific configurations. + // Network operator configurations are not supported. + if n.ingress || n.internal || n.attachable { + return types.ForbiddenErrorf("configuration network can only contain network " + + "specific fields. Network operator fields like " + + "[ ingress | internal | attachable ] are not supported.") + } + } + if n.configFrom != "" { + if n.configOnly { + return types.ForbiddenErrorf("a configuration network cannot depend on another configuration network") + } + if n.ipamType != "" && + n.ipamType != defaultIpamForNetworkType(n.networkType) || + n.enableIPv6 || + len(n.labels) > 0 || len(n.ipamOptions) > 0 || + len(n.ipamV4Config) > 0 || len(n.ipamV6Config) > 0 { + return types.ForbiddenErrorf("user specified configurations are not supported if the network depends on a configuration network") + } + if len(n.generic) > 0 { + if data, ok := n.generic[netlabel.GenericData]; ok { + var ( + driverOptions map[string]string + opts interface{} + ) + switch data.(type) { + case map[string]interface{}: + opts = data.(map[string]interface{}) + case map[string]string: + opts = data.(map[string]string) + } + ba, err := json.Marshal(opts) + if err != nil { + return fmt.Errorf("failed to validate network configuration: %v", err) + } + if err := json.Unmarshal(ba, &driverOptions); err != nil { + return fmt.Errorf("failed to validate network configuration: %v", err) + } + if len(driverOptions) > 0 { + return types.ForbiddenErrorf("network driver options are not supported if the network depends on a configuration network") + } + } + } + } + return nil +} + +// Applies network specific configurations +func (n *network) applyConfigurationTo(to *network) error { + to.enableIPv6 = n.enableIPv6 + if len(n.labels) > 0 { + to.labels = make(map[string]string, len(n.labels)) + for k, v := range n.labels { + if _, ok := to.labels[k]; !ok { + to.labels[k] = v + } + } + } + if len(n.ipamOptions) > 0 { + to.ipamOptions = make(map[string]string, len(n.ipamOptions)) + for k, v := range n.ipamOptions { + if _, ok := to.ipamOptions[k]; !ok { + to.ipamOptions[k] = v + } + } + } + if len(n.ipamV4Config) > 0 { + to.ipamV4Config = make([]*IpamConf, 0, len(n.ipamV4Config)) + for _, v4conf := range n.ipamV4Config { + to.ipamV4Config = append(to.ipamV4Config, v4conf) + } + } + if len(n.ipamV6Config) > 0 { + to.ipamV6Config = make([]*IpamConf, 0, len(n.ipamV6Config)) + for _, v6conf := range n.ipamV6Config { + to.ipamV6Config = append(to.ipamV6Config, v6conf) + } + } + if len(n.generic) > 0 { + to.generic = options.Generic{} + for k, v := range n.generic { + to.generic[k] = v + } + } + return nil +} + func (n *network) CopyTo(o datastore.KVObject) error { n.Lock() defer n.Unlock() @@ -370,6 +463,8 @@ func (n *network) CopyTo(o datastore.KVObject) error { dstN.attachable = n.attachable dstN.inDelete = n.inDelete dstN.ingress = n.ingress + dstN.configOnly = n.configOnly + dstN.configFrom = n.configFrom // copy labels if dstN.labels == nil { @@ -479,6 +574,8 @@ func (n *network) MarshalJSON() ([]byte, error) { netMap["attachable"] = n.attachable netMap["inDelete"] = n.inDelete netMap["ingress"] = n.ingress + netMap["configOnly"] = n.configOnly + netMap["configFrom"] = n.configFrom return json.Marshal(netMap) } @@ -583,6 +680,12 @@ func (n *network) UnmarshalJSON(b []byte) (err error) { if v, ok := netMap["ingress"]; ok { n.ingress = v.(bool) } + if v, ok := netMap["configOnly"]; ok { + n.configOnly = v.(bool) + } + if v, ok := netMap["configFrom"]; ok { + n.configFrom = v.(string) + } // Reconcile old networks with the recently added `--ipv6` flag if !n.enableIPv6 { n.enableIPv6 = len(n.ipamV6Info) > 0 @@ -713,6 +816,23 @@ func NetworkOptionDeferIPv6Alloc(enable bool) NetworkOption { } } +// NetworkOptionConfigOnly tells controller this network is +// a configuration only network. It serves as a configuration +// for other networks. +func NetworkOptionConfigOnly() NetworkOption { + return func(n *network) { + n.configOnly = true + } +} + +// NetworkOptionConfigFrom tells controller to pick the +// network configuration from a configuration only network +func NetworkOptionConfigFrom(name string) NetworkOption { + return func(n *network) { + n.configFrom = name + } +} + func (n *network) processOptions(options ...NetworkOption) { for _, opt := range options { if opt != nil { @@ -797,6 +917,9 @@ func (n *network) delete(force bool) error { } if !force && n.getEpCnt().EndpointCnt() != 0 { + if n.configOnly { + return types.ForbiddenErrorf("configuration network %q is in use", n.Name()) + } return &ActiveEndpointsError{name: n.name, id: n.id} } @@ -806,6 +929,21 @@ func (n *network) delete(force bool) error { return fmt.Errorf("error marking network %s (%s) for deletion: %v", n.Name(), n.ID(), err) } + if n.ConfigFrom() != "" { + if t, err := c.getConfigNetwork(n.ConfigFrom()); err == nil { + if err := t.getEpCnt().DecEndpointCnt(); err != nil { + logrus.Warnf("Failed to update reference count for configuration network %q on removal of network %q: %v", + t.Name(), n.Name(), err) + } + } else { + logrus.Warnf("Could not find configuration network %q during removal of network %q", n.configOnly, n.Name()) + } + } + + if n.configOnly { + goto removeFromStore + } + if err = n.deleteNetwork(); err != nil { if !force { return err @@ -831,6 +969,7 @@ func (n *network) delete(force bool) error { c.cleanupServiceBindings(n.ID()) +removeFromStore: // deleteFromStore performs an atomic delete operation and the // network.epCnt will help prevent any possible // race between endpoint join and network delete @@ -892,6 +1031,10 @@ func (n *network) CreateEndpoint(name string, options ...EndpointOption) (Endpoi return nil, ErrInvalidName(name) } + if n.ConfigOnly() { + return nil, types.ForbiddenErrorf("cannot create endpoint on configuration-only network") + } + if _, err = n.EndpointByName(name); err == nil { return nil, types.ForbiddenErrorf("endpoint with name %s already exists in network %s", name, n.Name()) } @@ -1611,6 +1754,20 @@ func (n *network) IPv6Enabled() bool { return n.enableIPv6 } +func (n *network) ConfigFrom() string { + n.Lock() + defer n.Unlock() + + return n.configFrom +} + +func (n *network) ConfigOnly() bool { + n.Lock() + defer n.Unlock() + + return n.configOnly +} + func (n *network) Labels() map[string]string { n.Lock() defer n.Unlock() @@ -1778,3 +1935,24 @@ func (n *network) ExecFunc(f func()) error { func (n *network) NdotsSet() bool { return false } + +// config-only network is looked up by name +func (c *controller) getConfigNetwork(name string) (*network, error) { + var n Network + + s := func(current Network) bool { + if current.Info().ConfigOnly() && current.Name() == name { + n = current + return true + } + return false + } + + c.WalkNetworks(s) + + if n == nil { + return nil, types.NotFoundErrorf("configuration network %q not found", name) + } + + return n.(*network), nil +} diff --git a/libnetwork/store.go b/libnetwork/store.go index 58e1d852f1..cb220561b6 100644 --- a/libnetwork/store.go +++ b/libnetwork/store.go @@ -478,7 +478,7 @@ func (c *controller) networkCleanup() { } var populateSpecial NetworkWalker = func(nw Network) bool { - if n := nw.(*network); n.hasSpecialDriver() { + if n := nw.(*network); n.hasSpecialDriver() && !n.ConfigOnly() { if err := n.getController().addNetwork(n); err != nil { logrus.Warnf("Failed to populate network %q with driver %q", nw.Name(), nw.Type()) }