mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Integrate local datascope network with swarm
Signed-off-by: Alessandro Boch <aboch@docker.com>
This commit is contained in:
parent
fcafc7108b
commit
b34d3e730f
6 changed files with 124 additions and 34 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
package swarm
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
)
|
||||||
|
|
||||||
// Endpoint represents an endpoint.
|
// Endpoint represents an endpoint.
|
||||||
type Endpoint struct {
|
type Endpoint struct {
|
||||||
Spec EndpointSpec `json:",omitempty"`
|
Spec EndpointSpec `json:",omitempty"`
|
||||||
|
|
@ -78,12 +82,14 @@ type Network struct {
|
||||||
// NetworkSpec represents the spec of a network.
|
// NetworkSpec represents the spec of a network.
|
||||||
type NetworkSpec struct {
|
type NetworkSpec struct {
|
||||||
Annotations
|
Annotations
|
||||||
DriverConfiguration *Driver `json:",omitempty"`
|
DriverConfiguration *Driver `json:",omitempty"`
|
||||||
IPv6Enabled bool `json:",omitempty"`
|
IPv6Enabled bool `json:",omitempty"`
|
||||||
Internal bool `json:",omitempty"`
|
Internal bool `json:",omitempty"`
|
||||||
Attachable bool `json:",omitempty"`
|
Attachable bool `json:",omitempty"`
|
||||||
Ingress bool `json:",omitempty"`
|
Ingress bool `json:",omitempty"`
|
||||||
IPAMOptions *IPAMOptions `json:",omitempty"`
|
IPAMOptions *IPAMOptions `json:",omitempty"`
|
||||||
|
ConfigFrom *network.ConfigReference `json:",omitempty"`
|
||||||
|
Scope string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkAttachmentConfig represents the configuration of a network attachment.
|
// NetworkAttachmentConfig represents the configuration of a network attachment.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
basictypes "github.com/docker/docker/api/types"
|
basictypes "github.com/docker/docker/api/types"
|
||||||
networktypes "github.com/docker/docker/api/types/network"
|
networktypes "github.com/docker/docker/api/types/network"
|
||||||
types "github.com/docker/docker/api/types/swarm"
|
types "github.com/docker/docker/api/types/swarm"
|
||||||
|
netconst "github.com/docker/libnetwork/datastore"
|
||||||
swarmapi "github.com/docker/swarmkit/api"
|
swarmapi "github.com/docker/swarmkit/api"
|
||||||
gogotypes "github.com/gogo/protobuf/types"
|
gogotypes "github.com/gogo/protobuf/types"
|
||||||
)
|
)
|
||||||
|
|
@ -30,10 +31,17 @@ func networkFromGRPC(n *swarmapi.Network) types.Network {
|
||||||
Attachable: n.Spec.Attachable,
|
Attachable: n.Spec.Attachable,
|
||||||
Ingress: n.Spec.Ingress,
|
Ingress: n.Spec.Ingress,
|
||||||
IPAMOptions: ipamFromGRPC(n.Spec.IPAM),
|
IPAMOptions: ipamFromGRPC(n.Spec.IPAM),
|
||||||
|
Scope: netconst.SwarmScope,
|
||||||
},
|
},
|
||||||
IPAMOptions: ipamFromGRPC(n.IPAM),
|
IPAMOptions: ipamFromGRPC(n.IPAM),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if n.Spec.ConfigFrom != "" {
|
||||||
|
network.Spec.ConfigFrom = &networktypes.ConfigReference{
|
||||||
|
Network: n.Spec.ConfigFrom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Meta
|
// Meta
|
||||||
network.Version.Index = n.Meta.Version.Index
|
network.Version.Index = n.Meta.Version.Index
|
||||||
network.CreatedAt, _ = gogotypes.TimestampFromProto(n.Meta.CreatedAt)
|
network.CreatedAt, _ = gogotypes.TimestampFromProto(n.Meta.CreatedAt)
|
||||||
|
|
@ -152,7 +160,7 @@ func BasicNetworkFromGRPC(n swarmapi.Network) basictypes.NetworkResource {
|
||||||
nr := basictypes.NetworkResource{
|
nr := basictypes.NetworkResource{
|
||||||
ID: n.ID,
|
ID: n.ID,
|
||||||
Name: n.Spec.Annotations.Name,
|
Name: n.Spec.Annotations.Name,
|
||||||
Scope: "swarm",
|
Scope: netconst.SwarmScope,
|
||||||
EnableIPv6: spec.Ipv6Enabled,
|
EnableIPv6: spec.Ipv6Enabled,
|
||||||
IPAM: ipam,
|
IPAM: ipam,
|
||||||
Internal: spec.Internal,
|
Internal: spec.Internal,
|
||||||
|
|
@ -161,6 +169,12 @@ func BasicNetworkFromGRPC(n swarmapi.Network) basictypes.NetworkResource {
|
||||||
Labels: n.Spec.Annotations.Labels,
|
Labels: n.Spec.Annotations.Labels,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if n.Spec.ConfigFrom != "" {
|
||||||
|
nr.ConfigFrom = networktypes.ConfigReference{
|
||||||
|
Network: n.Spec.ConfigFrom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if n.DriverState != nil {
|
if n.DriverState != nil {
|
||||||
nr.Driver = n.DriverState.Name
|
nr.Driver = n.DriverState.Name
|
||||||
nr.Options = n.DriverState.Options
|
nr.Options = n.DriverState.Options
|
||||||
|
|
@ -206,5 +220,8 @@ func BasicNetworkCreateToGRPC(create basictypes.NetworkCreateRequest) swarmapi.N
|
||||||
}
|
}
|
||||||
ns.IPAM.Configs = ipamSpec
|
ns.IPAM.Configs = ipamSpec
|
||||||
}
|
}
|
||||||
|
if create.ConfigFrom != nil {
|
||||||
|
ns.ConfigFrom = create.ConfigFrom.Network
|
||||||
|
}
|
||||||
return ns
|
return ns
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ func (c *containerAdapter) removeNetworks(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerAdapter) networkAttach(ctx context.Context) error {
|
func (c *containerAdapter) networkAttach(ctx context.Context) error {
|
||||||
config := c.container.createNetworkingConfig()
|
config := c.container.createNetworkingConfig(c.backend)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
networkName string
|
networkName string
|
||||||
|
|
@ -195,7 +195,7 @@ func (c *containerAdapter) networkAttach(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerAdapter) waitForDetach(ctx context.Context) error {
|
func (c *containerAdapter) waitForDetach(ctx context.Context) error {
|
||||||
config := c.container.createNetworkingConfig()
|
config := c.container.createNetworkingConfig(c.backend)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
networkName string
|
networkName string
|
||||||
|
|
@ -216,20 +216,19 @@ func (c *containerAdapter) waitForDetach(ctx context.Context) error {
|
||||||
func (c *containerAdapter) create(ctx context.Context) error {
|
func (c *containerAdapter) create(ctx context.Context) error {
|
||||||
var cr containertypes.ContainerCreateCreatedBody
|
var cr containertypes.ContainerCreateCreatedBody
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if cr, err = c.backend.CreateManagedContainer(types.ContainerCreateConfig{
|
if cr, err = c.backend.CreateManagedContainer(types.ContainerCreateConfig{
|
||||||
Name: c.container.name(),
|
Name: c.container.name(),
|
||||||
Config: c.container.config(),
|
Config: c.container.config(),
|
||||||
HostConfig: c.container.hostConfig(),
|
HostConfig: c.container.hostConfig(),
|
||||||
// Use the first network in container create
|
// Use the first network in container create
|
||||||
NetworkingConfig: c.container.createNetworkingConfig(),
|
NetworkingConfig: c.container.createNetworkingConfig(c.backend),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Docker daemon currently doesn't support multiple networks in container create
|
// Docker daemon currently doesn't support multiple networks in container create
|
||||||
// Connect to all other networks
|
// Connect to all other networks
|
||||||
nc := c.container.connectNetworkingConfig()
|
nc := c.container.connectNetworkingConfig(c.backend)
|
||||||
|
|
||||||
if nc != nil {
|
if nc != nil {
|
||||||
for n, ep := range nc.EndpointsConfig {
|
for n, ep := range nc.EndpointsConfig {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ import (
|
||||||
enginemount "github.com/docker/docker/api/types/mount"
|
enginemount "github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
volumetypes "github.com/docker/docker/api/types/volume"
|
volumetypes "github.com/docker/docker/api/types/volume"
|
||||||
|
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
||||||
clustertypes "github.com/docker/docker/daemon/cluster/provider"
|
clustertypes "github.com/docker/docker/daemon/cluster/provider"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
netconst "github.com/docker/libnetwork/datastore"
|
||||||
"github.com/docker/swarmkit/agent/exec"
|
"github.com/docker/swarmkit/agent/exec"
|
||||||
"github.com/docker/swarmkit/api"
|
"github.com/docker/swarmkit/api"
|
||||||
"github.com/docker/swarmkit/template"
|
"github.com/docker/swarmkit/template"
|
||||||
|
|
@ -374,6 +376,14 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(c.task.Networks) > 0 {
|
||||||
|
labels := c.task.Networks[0].Network.Spec.Annotations.Labels
|
||||||
|
name := c.task.Networks[0].Network.Spec.Annotations.Name
|
||||||
|
if v, ok := labels["com.docker.swarm.predefined"]; ok && v == "true" {
|
||||||
|
hc.NetworkMode = enginecontainer.NetworkMode(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return hc
|
return hc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,7 +438,7 @@ func (c *containerConfig) resources() enginecontainer.Resources {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Docker daemon supports just 1 network during container create.
|
// Docker daemon supports just 1 network during container create.
|
||||||
func (c *containerConfig) createNetworkingConfig() *network.NetworkingConfig {
|
func (c *containerConfig) createNetworkingConfig(b executorpkg.Backend) *network.NetworkingConfig {
|
||||||
var networks []*api.NetworkAttachment
|
var networks []*api.NetworkAttachment
|
||||||
if c.task.Spec.GetContainer() != nil || c.task.Spec.GetAttachment() != nil {
|
if c.task.Spec.GetContainer() != nil || c.task.Spec.GetAttachment() != nil {
|
||||||
networks = c.task.Networks
|
networks = c.task.Networks
|
||||||
|
|
@ -436,19 +446,18 @@ func (c *containerConfig) createNetworkingConfig() *network.NetworkingConfig {
|
||||||
|
|
||||||
epConfig := make(map[string]*network.EndpointSettings)
|
epConfig := make(map[string]*network.EndpointSettings)
|
||||||
if len(networks) > 0 {
|
if len(networks) > 0 {
|
||||||
epConfig[networks[0].Network.Spec.Annotations.Name] = getEndpointConfig(networks[0])
|
epConfig[networks[0].Network.Spec.Annotations.Name] = getEndpointConfig(networks[0], b)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &network.NetworkingConfig{EndpointsConfig: epConfig}
|
return &network.NetworkingConfig{EndpointsConfig: epConfig}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Merge this function with createNetworkingConfig after daemon supports multiple networks in container create
|
// TODO: Merge this function with createNetworkingConfig after daemon supports multiple networks in container create
|
||||||
func (c *containerConfig) connectNetworkingConfig() *network.NetworkingConfig {
|
func (c *containerConfig) connectNetworkingConfig(b executorpkg.Backend) *network.NetworkingConfig {
|
||||||
var networks []*api.NetworkAttachment
|
var networks []*api.NetworkAttachment
|
||||||
if c.task.Spec.GetContainer() != nil {
|
if c.task.Spec.GetContainer() != nil {
|
||||||
networks = c.task.Networks
|
networks = c.task.Networks
|
||||||
}
|
}
|
||||||
|
|
||||||
// First network is used during container create. Other networks are used in "docker network connect"
|
// First network is used during container create. Other networks are used in "docker network connect"
|
||||||
if len(networks) < 2 {
|
if len(networks) < 2 {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -456,12 +465,12 @@ func (c *containerConfig) connectNetworkingConfig() *network.NetworkingConfig {
|
||||||
|
|
||||||
epConfig := make(map[string]*network.EndpointSettings)
|
epConfig := make(map[string]*network.EndpointSettings)
|
||||||
for _, na := range networks[1:] {
|
for _, na := range networks[1:] {
|
||||||
epConfig[na.Network.Spec.Annotations.Name] = getEndpointConfig(na)
|
epConfig[na.Network.Spec.Annotations.Name] = getEndpointConfig(na, b)
|
||||||
}
|
}
|
||||||
return &network.NetworkingConfig{EndpointsConfig: epConfig}
|
return &network.NetworkingConfig{EndpointsConfig: epConfig}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEndpointConfig(na *api.NetworkAttachment) *network.EndpointSettings {
|
func getEndpointConfig(na *api.NetworkAttachment, b executorpkg.Backend) *network.EndpointSettings {
|
||||||
var ipv4, ipv6 string
|
var ipv4, ipv6 string
|
||||||
for _, addr := range na.Addresses {
|
for _, addr := range na.Addresses {
|
||||||
ip, _, err := net.ParseCIDR(addr)
|
ip, _, err := net.ParseCIDR(addr)
|
||||||
|
|
@ -479,13 +488,19 @@ func getEndpointConfig(na *api.NetworkAttachment) *network.EndpointSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &network.EndpointSettings{
|
n := &network.EndpointSettings{
|
||||||
NetworkID: na.Network.ID,
|
NetworkID: na.Network.ID,
|
||||||
IPAMConfig: &network.EndpointIPAMConfig{
|
IPAMConfig: &network.EndpointIPAMConfig{
|
||||||
IPv4Address: ipv4,
|
IPv4Address: ipv4,
|
||||||
IPv6Address: ipv6,
|
IPv6Address: ipv6,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if v, ok := na.Network.Spec.Annotations.Labels["com.docker.swarm.predefined"]; ok && v == "true" {
|
||||||
|
if ln, err := b.FindNetwork(na.Network.Spec.Annotations.Name); err == nil {
|
||||||
|
n.NetworkID = ln.ID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerConfig) virtualIP(networkID string) string {
|
func (c *containerConfig) virtualIP(networkID string) string {
|
||||||
|
|
@ -570,27 +585,38 @@ func (c *containerConfig) networkCreateRequest(name string) (clustertypes.Networ
|
||||||
|
|
||||||
options := types.NetworkCreate{
|
options := types.NetworkCreate{
|
||||||
// ID: na.Network.ID,
|
// ID: na.Network.ID,
|
||||||
Driver: na.Network.DriverState.Name,
|
|
||||||
IPAM: &network.IPAM{
|
|
||||||
Driver: na.Network.IPAM.Driver.Name,
|
|
||||||
Options: na.Network.IPAM.Driver.Options,
|
|
||||||
},
|
|
||||||
Options: na.Network.DriverState.Options,
|
|
||||||
Labels: na.Network.Spec.Annotations.Labels,
|
Labels: na.Network.Spec.Annotations.Labels,
|
||||||
Internal: na.Network.Spec.Internal,
|
Internal: na.Network.Spec.Internal,
|
||||||
Attachable: na.Network.Spec.Attachable,
|
Attachable: na.Network.Spec.Attachable,
|
||||||
Ingress: na.Network.Spec.Ingress,
|
Ingress: na.Network.Spec.Ingress,
|
||||||
EnableIPv6: na.Network.Spec.Ipv6Enabled,
|
EnableIPv6: na.Network.Spec.Ipv6Enabled,
|
||||||
CheckDuplicate: true,
|
CheckDuplicate: true,
|
||||||
|
Scope: netconst.SwarmScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ic := range na.Network.IPAM.Configs {
|
if na.Network.Spec.ConfigFrom != "" {
|
||||||
c := network.IPAMConfig{
|
options.ConfigFrom = &network.ConfigReference{
|
||||||
Subnet: ic.Subnet,
|
Network: na.Network.Spec.ConfigFrom,
|
||||||
IPRange: ic.Range,
|
}
|
||||||
Gateway: ic.Gateway,
|
}
|
||||||
|
|
||||||
|
if na.Network.DriverState != nil {
|
||||||
|
options.Driver = na.Network.DriverState.Name
|
||||||
|
options.Options = na.Network.DriverState.Options
|
||||||
|
}
|
||||||
|
if na.Network.IPAM != nil {
|
||||||
|
options.IPAM = &network.IPAM{
|
||||||
|
Driver: na.Network.IPAM.Driver.Name,
|
||||||
|
Options: na.Network.IPAM.Driver.Options,
|
||||||
|
}
|
||||||
|
for _, ic := range na.Network.IPAM.Configs {
|
||||||
|
c := network.IPAMConfig{
|
||||||
|
Subnet: ic.Subnet,
|
||||||
|
IPRange: ic.Range,
|
||||||
|
Gateway: ic.Gateway,
|
||||||
|
}
|
||||||
|
options.IPAM.Config = append(options.IPAM.Config, c)
|
||||||
}
|
}
|
||||||
options.IPAM.Config = append(options.IPAM.Config, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return clustertypes.NetworkCreateRequest{
|
return clustertypes.NetworkCreateRequest{
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,28 @@ import (
|
||||||
|
|
||||||
// GetNetworks returns all current cluster managed networks.
|
// GetNetworks returns all current cluster managed networks.
|
||||||
func (c *Cluster) GetNetworks() ([]apitypes.NetworkResource, error) {
|
func (c *Cluster) GetNetworks() ([]apitypes.NetworkResource, error) {
|
||||||
return c.getNetworks(nil)
|
list, err := c.getNetworks(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
removePredefinedNetworks(&list)
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePredefinedNetworks(networks *[]apitypes.NetworkResource) {
|
||||||
|
if networks == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var idxs []int
|
||||||
|
for i, n := range *networks {
|
||||||
|
if v, ok := n.Labels["com.docker.swarm.predefined"]; ok && v == "true" {
|
||||||
|
idxs = append(idxs, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, idx := range idxs {
|
||||||
|
idx -= i
|
||||||
|
*networks = append((*networks)[:idx], (*networks)[idx+1:]...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cluster) getNetworks(filters *swarmapi.ListNetworksRequest_Filters) ([]apitypes.NetworkResource, error) {
|
func (c *Cluster) getNetworks(filters *swarmapi.ListNetworksRequest_Filters) ([]apitypes.NetworkResource, error) {
|
||||||
|
|
@ -269,16 +290,27 @@ func (c *Cluster) populateNetworkID(ctx context.Context, client swarmapi.Control
|
||||||
if len(networks) == 0 {
|
if len(networks) == 0 {
|
||||||
networks = s.Networks
|
networks = s.Networks
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, n := range networks {
|
for i, n := range networks {
|
||||||
apiNetwork, err := getNetwork(ctx, client, n.Target)
|
apiNetwork, err := getNetwork(ctx, client, n.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ln, _ := c.config.Backend.FindNetwork(n.Target); ln != nil && !ln.Info().Dynamic() {
|
ln, _ := c.config.Backend.FindNetwork(n.Target)
|
||||||
|
if ln != nil && runconfig.IsPreDefinedNetwork(ln.Name()) {
|
||||||
|
// Need to retrieve the corresponding predefined swarm network
|
||||||
|
// and use its id for the request.
|
||||||
|
apiNetwork, err = getNetwork(ctx, client, ln.Name())
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("could not find the corresponding predefined swarm network: %v", err)
|
||||||
|
return apierrors.NewRequestNotFoundError(err)
|
||||||
|
}
|
||||||
|
goto setid
|
||||||
|
}
|
||||||
|
if ln != nil && !ln.Info().Dynamic() {
|
||||||
err = fmt.Errorf("The network %s cannot be used with services. Only networks scoped to the swarm can be used, such as those created with the overlay driver.", ln.Name())
|
err = fmt.Errorf("The network %s cannot be used with services. Only networks scoped to the swarm can be used, such as those created with the overlay driver.", ln.Name())
|
||||||
return apierrors.NewRequestForbiddenError(err)
|
return apierrors.NewRequestForbiddenError(err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
setid:
|
||||||
networks[i].Target = apiNetwork.ID
|
networks[i].Target = apiNetwork.ID
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,16 @@ func (daemon *Daemon) deleteNetwork(networkID string, dynamic bool) error {
|
||||||
return apierrors.NewRequestForbiddenError(err)
|
return apierrors.NewRequestForbiddenError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dynamic && !nw.Info().Dynamic() {
|
||||||
|
if runconfig.IsPreDefinedNetwork(nw.Name()) {
|
||||||
|
// Predefined networks now support swarm services. Make this
|
||||||
|
// a no-op when cluster requests to remove the predefined network.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := fmt.Errorf("%s is not a dynamic network", nw.Name())
|
||||||
|
return apierrors.NewRequestForbiddenError(err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := nw.Delete(); err != nil {
|
if err := nw.Delete(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue