summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbaude <bbaude@redhat.com>2020-11-11 09:45:07 -0600
committerbaude <bbaude@redhat.com>2020-11-19 08:16:19 -0600
commita3e0b7d117251944375fd32449f6b26d65edf367 (patch)
tree824e4acb1529fb72276630fdd4d57127c07aadc8
parent286d356db03a2511722155022c19855f8aee73cf (diff)
downloadpodman-a3e0b7d117251944375fd32449f6b26d65edf367.tar.gz
podman-a3e0b7d117251944375fd32449f6b26d65edf367.tar.bz2
podman-a3e0b7d117251944375fd32449f6b26d65edf367.zip
add network connect|disconnect compat endpoints
this enables the ability to connect and disconnect a container from a given network. it is only for the compatibility layer. some code had to be refactored to avoid circular imports. additionally, tests are being deferred temporarily due to some incompatibility/bug in either docker-py or our stack. Signed-off-by: baude <bbaude@redhat.com>
-rw-r--r--cmd/podman/networks/connect.go47
-rw-r--r--cmd/podman/networks/disconnect.go45
-rw-r--r--commands-demo.md2
-rw-r--r--docs/source/markdown/podman-network-connect.1.md34
-rw-r--r--docs/source/markdown/podman-network-disconnect.1.md29
-rw-r--r--docs/source/markdown/podman-network.1.md2
-rw-r--r--docs/source/network.rst4
-rw-r--r--libpod/container.go24
-rw-r--r--libpod/define/errors.go3
-rw-r--r--libpod/events.go12
-rw-r--r--libpod/events/config.go8
-rw-r--r--libpod/events/events.go8
-rw-r--r--libpod/events/journal_linux.go6
-rw-r--r--libpod/events/logfile.go2
-rw-r--r--libpod/networking_linux.go179
-rw-r--r--pkg/api/handlers/libpod/networks.go26
-rw-r--r--pkg/api/server/register_networks.go54
-rw-r--r--pkg/bindings/network/network.go38
-rw-r--r--pkg/domain/entities/engine_container.go2
-rw-r--r--pkg/domain/entities/network.go14
-rw-r--r--pkg/domain/infra/abi/network.go9
-rw-r--r--pkg/domain/infra/tunnel/network.go10
-rw-r--r--test/e2e/network_test.go153
23 files changed, 651 insertions, 60 deletions
diff --git a/cmd/podman/networks/connect.go b/cmd/podman/networks/connect.go
new file mode 100644
index 000000000..a7636688c
--- /dev/null
+++ b/cmd/podman/networks/connect.go
@@ -0,0 +1,47 @@
+package network
+
+import (
+ "github.com/containers/common/pkg/completion"
+ "github.com/containers/podman/v2/cmd/podman/common"
+ "github.com/containers/podman/v2/cmd/podman/registry"
+ "github.com/containers/podman/v2/pkg/domain/entities"
+ "github.com/spf13/cobra"
+)
+
+var (
+ networkConnectDescription = `Add container to a network`
+ networkConnectCommand = &cobra.Command{
+ Use: "connect [options] NETWORK CONTAINER",
+ Short: "network connect",
+ Long: networkConnectDescription,
+ RunE: networkConnect,
+ Example: `podman network connect web secondary`,
+ Args: cobra.ExactArgs(2),
+ ValidArgsFunction: common.AutocompleteNetworks,
+ }
+)
+
+var (
+ networkConnectOptions entities.NetworkConnectOptions
+)
+
+func networkConnectFlags(cmd *cobra.Command) {
+ flags := cmd.Flags()
+ aliasFlagName := "alias"
+ flags.StringSliceVar(&networkConnectOptions.Aliases, aliasFlagName, []string{}, "network scoped alias for container")
+ _ = cmd.RegisterFlagCompletionFunc(aliasFlagName, completion.AutocompleteNone)
+}
+
+func init() {
+ registry.Commands = append(registry.Commands, registry.CliCommand{
+ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+ Command: networkConnectCommand,
+ Parent: networkCmd,
+ })
+ networkConnectFlags(networkConnectCommand)
+}
+
+func networkConnect(cmd *cobra.Command, args []string) error {
+ networkConnectOptions.Container = args[1]
+ return registry.ContainerEngine().NetworkConnect(registry.Context(), args[0], networkConnectOptions)
+}
diff --git a/cmd/podman/networks/disconnect.go b/cmd/podman/networks/disconnect.go
new file mode 100644
index 000000000..598c23a1c
--- /dev/null
+++ b/cmd/podman/networks/disconnect.go
@@ -0,0 +1,45 @@
+package network
+
+import (
+ "github.com/containers/podman/v2/cmd/podman/common"
+ "github.com/containers/podman/v2/cmd/podman/registry"
+ "github.com/containers/podman/v2/pkg/domain/entities"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+)
+
+var (
+ networkDisconnectDescription = `Remove container from a network`
+ networkDisconnectCommand = &cobra.Command{
+ Use: "disconnect [options] NETWORK CONTAINER",
+ Short: "network rm",
+ Long: networkDisconnectDescription,
+ RunE: networkDisconnect,
+ Example: `podman network disconnect web secondary`,
+ Args: cobra.ExactArgs(2),
+ ValidArgsFunction: common.AutocompleteNetworks,
+ }
+)
+
+var (
+ networkDisconnectOptions entities.NetworkDisconnectOptions
+)
+
+func networkDisconnectFlags(flags *pflag.FlagSet) {
+ flags.BoolVarP(&networkDisconnectOptions.Force, "force", "f", false, "force removal of container from network")
+}
+
+func init() {
+ registry.Commands = append(registry.Commands, registry.CliCommand{
+ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+ Command: networkDisconnectCommand,
+ Parent: networkCmd,
+ })
+ flags := networkDisconnectCommand.Flags()
+ networkDisconnectFlags(flags)
+}
+
+func networkDisconnect(cmd *cobra.Command, args []string) error {
+ networkDisconnectOptions.Container = args[1]
+ return registry.ContainerEngine().NetworkDisconnect(registry.Context(), args[0], networkDisconnectOptions)
+}
diff --git a/commands-demo.md b/commands-demo.md
index 74879842f..cea7a63a3 100644
--- a/commands-demo.md
+++ b/commands-demo.md
@@ -47,6 +47,8 @@
| [podman-mount(1)](https://podman.readthedocs.io/en/latest/markdown/podman-mount.1.html) | Mount a working container's root filesystem |
| [podman-network(1)](https://podman.readthedocs.io/en/latest/network.html) | Manage Podman CNI networks |
| [podman-network-create(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-create.1.html) | Create a CNI network |
+| [podman-network-connect(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-connect.1.html) | Connect a container to a CNI network |
+| [podman-network-disconnect(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-dosconnect.1.html) | Disconnect a container from a CNI network |
| [podman-network-inspect(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-inspect.1.html) | Displays the raw CNI network configuration for one or more networks |
| [podman-network-ls(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-ls.1.html) | Display a summary of CNI networks |
| [podman-network-rm(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-rm.1.html) | Remove one or more CNI networks |
diff --git a/docs/source/markdown/podman-network-connect.1.md b/docs/source/markdown/podman-network-connect.1.md
new file mode 100644
index 000000000..58b6e5c44
--- /dev/null
+++ b/docs/source/markdown/podman-network-connect.1.md
@@ -0,0 +1,34 @@
+% podman-network-connect(1)
+
+## NAME
+podman\-network\-connect - Connect a container to a network
+
+## SYNOPSIS
+**podman network connect** [*options*] network container
+
+## DESCRIPTION
+Connects a container to a network. A container can be connected to a network by name or by ID.
+Once connected, the container can communicate with other containers in the same network.
+
+## OPTIONS
+#### **--alias**
+Add network-scoped alias for the container. If the network is using the `dnsname` CNI plugin, these aliases
+can be used for name resolution on the given network. Multiple *--alias* options may be specificed as input.
+
+## EXAMPLE
+
+Connect a container named *web* to a network named *test*
+```
+podman network connect test web
+```
+
+Connect a container name *web* to a network named *test* with two aliases: web1 and web2
+```
+podman network connect --alias web1 --alias web2 test web
+```
+
+## SEE ALSO
+podman(1), podman-network(1), podman-network-disconnect(1), podman-network-inspect(1)
+
+## HISTORY
+November 2020, Originally compiled by Brent Baude <bbaude@redhat.com>
diff --git a/docs/source/markdown/podman-network-disconnect.1.md b/docs/source/markdown/podman-network-disconnect.1.md
new file mode 100644
index 000000000..95c7018a8
--- /dev/null
+++ b/docs/source/markdown/podman-network-disconnect.1.md
@@ -0,0 +1,29 @@
+% podman-network-disconnect(1)
+
+## NAME
+podman\-network\-disconnect - Disconnect a container from a network
+
+## SYNOPSIS
+**podman network disconnect** [*options*] network container
+
+## DESCRIPTION
+Disconnects a container from a network.
+
+## OPTIONS
+#### **--force**, **-f**
+
+Force the container to disconnect from a network
+
+## EXAMPLE
+Disconnect a container named *web* from a network called *test*.
+
+```
+podman network disconnect test web
+```
+
+
+## SEE ALSO
+podman(1), podman-network(1), podman-network-connect(1)
+
+## HISTORY
+November 2020, Originally compiled by Brent Baude <bbaude@redhat.com>
diff --git a/docs/source/markdown/podman-network.1.md b/docs/source/markdown/podman-network.1.md
index f05b2b78f..d21b200d9 100644
--- a/docs/source/markdown/podman-network.1.md
+++ b/docs/source/markdown/podman-network.1.md
@@ -13,7 +13,9 @@ The network command manages CNI networks for Podman. It is not supported for roo
| Command | Man Page | Description |
| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
+| connect | [podman-network-connect(1)](podman-network-connect.1.md)| Connect a container to a network|
| create | [podman-network-create(1)](podman-network-create.1.md)| Create a Podman CNI network|
+| disconnect | [podman-network-disconnect(1)](podman-network-disconnect.1.md)| Disconnect a container from a network|
| inspect | [podman-network-inspect(1)](podman-network-inspect.1.md)| Displays the raw CNI network configuration for one or more networks|
| ls | [podman-network-ls(1)](podman-network-ls.1.md)| Display a summary of CNI networks |
| rm | [podman-network-rm(1)](podman-network-rm.1.md)| Remove one or more CNI networks |
diff --git a/docs/source/network.rst b/docs/source/network.rst
index e7848c90e..0414c0880 100644
--- a/docs/source/network.rst
+++ b/docs/source/network.rst
@@ -1,8 +1,12 @@
Network
=======
+:doc:`connect <markdown/podman-network-connect.1>` network connect
+
:doc:`create <markdown/podman-network-create.1>` network create
+:doc:`disconnect <markdown/podman-network-disconnect.1>` network disconnect
+
:doc:`inspect <markdown/podman-network-inspect.1>` network inspect
:doc:`ls <markdown/podman-network-ls.1>` network list
diff --git a/libpod/container.go b/libpod/container.go
index 9009a4ec8..4b9e6a5ba 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -206,6 +206,10 @@ type ContainerState struct {
// and not delegated to the OCI runtime.
ExtensionStageHooks map[string][]spec.Hook `json:"extensionStageHooks,omitempty"`
+ // NetInterfaceDescriptions describe the relationship between a CNI
+ // network and an interface names
+ NetInterfaceDescriptions ContainerNetworkDescriptions `json:"networkDescriptions,omitempty"`
+
// containerPlatformState holds platform-specific container state.
containerPlatformState
}
@@ -244,6 +248,10 @@ type ContainerImageVolume struct {
ReadWrite bool `json:"rw"`
}
+// ContainerNetworkDescriptions describes the relationship between the CNI
+// network and the ethN where N is an integer
+type ContainerNetworkDescriptions map[string]int
+
// Config accessors
// Unlocked
@@ -1102,3 +1110,19 @@ func (c *Container) networksByNameIndex() (map[string]int, error) {
}
return networkNamesByIndex, nil
}
+
+// add puts the new given CNI network name into the tracking map
+// and assigns it a new integer based on the map length
+func (d ContainerNetworkDescriptions) add(networkName string) {
+ d[networkName] = len(d)
+}
+
+// getInterfaceByName returns a formatted interface name for a given
+// network along with a bool as to whether the network existed
+func (d ContainerNetworkDescriptions) getInterfaceByName(networkName string) (string, bool) {
+ val, exists := d[networkName]
+ if !exists {
+ return "", exists
+ }
+ return fmt.Sprintf("eth%d", val), exists
+}
diff --git a/libpod/define/errors.go b/libpod/define/errors.go
index 471827b7c..b96d36429 100644
--- a/libpod/define/errors.go
+++ b/libpod/define/errors.go
@@ -178,4 +178,7 @@ var (
// ErrStoreNotInitialized indicates that the container storage was never
// initialized.
ErrStoreNotInitialized = errors.New("the container storage was never initialized")
+
+ // ErrNoNetwork indicates that a container has no net namespace, like network=none
+ ErrNoNetwork = errors.New("container has no network namespace")
)
diff --git a/libpod/events.go b/libpod/events.go
index 95317eb01..e199a3846 100644
--- a/libpod/events.go
+++ b/libpod/events.go
@@ -50,6 +50,18 @@ func (c *Container) newContainerExitedEvent(exitCode int32) {
}
}
+// netNetworkEvent creates a new event based on a network connect/disconnect
+func (c *Container) newNetworkEvent(status events.Status, netName string) {
+ e := events.NewEvent(status)
+ e.ID = c.ID()
+ e.Name = c.Name()
+ e.Type = events.Network
+ e.Network = netName
+ if err := c.runtime.eventer.Write(e); err != nil {
+ logrus.Errorf("unable to write pod event: %q", err)
+ }
+}
+
// newPodEvent creates a new event for a libpod pod
func (p *Pod) newPodEvent(status events.Status) {
e := events.NewEvent(status)
diff --git a/libpod/events/config.go b/libpod/events/config.go
index 2ec3111fe..af09a65ae 100644
--- a/libpod/events/config.go
+++ b/libpod/events/config.go
@@ -30,6 +30,8 @@ type Event struct {
Image string `json:",omitempty"`
// Name where applicable
Name string `json:",omitempty"`
+ // Network is the network name in a network event
+ Network string `json:"network,omitempty"`
// Status describes the event that occurred
Status Status
// Time the event occurred
@@ -101,6 +103,8 @@ const (
Container Type = "container"
// Image - event is related to images
Image Type = "image"
+ // Network - event is related to networks
+ Network Type = "network"
// Pod - event is related to pods
Pod Type = "pod"
// System - event is related to Podman whole and not to any specific
@@ -141,6 +145,10 @@ const (
LoadFromArchive Status = "loadfromarchive"
// Mount ...
Mount Status = "mount"
+ // NetworkConnect
+ NetworkConnect Status = "connect"
+ // NetworkDisconnect
+ NetworkDisconnect Status = "disconnect"
// Pause ...
Pause Status = "pause"
// Prune ...
diff --git a/libpod/events/events.go b/libpod/events/events.go
index 42939d64c..4e7267af3 100644
--- a/libpod/events/events.go
+++ b/libpod/events/events.go
@@ -77,6 +77,8 @@ func (e *Event) ToHumanReadable() string {
}
}
humanFormat += ")"
+ case Network:
+ humanFormat = fmt.Sprintf("%s %s %s %s (container=%s, name=%s)", e.Time, e.Type, e.Status, e.ID, e.ID, e.Network)
case Image:
humanFormat = fmt.Sprintf("%s %s %s %s %s", e.Time, e.Type, e.Status, e.ID, e.Name)
case System:
@@ -115,6 +117,8 @@ func StringToType(name string) (Type, error) {
return Container, nil
case Image.String():
return Image, nil
+ case Network.String():
+ return Network, nil
case Pod.String():
return Pod, nil
case System.String():
@@ -162,6 +166,10 @@ func StringToStatus(name string) (Status, error) {
return LoadFromArchive, nil
case Mount.String():
return Mount, nil
+ case NetworkConnect.String():
+ return NetworkConnect, nil
+ case NetworkDisconnect.String():
+ return NetworkDisconnect, nil
case Pause.String():
return Pause, nil
case Prune.String():
diff --git a/libpod/events/journal_linux.go b/libpod/events/journal_linux.go
index 5e3be8009..9a514e302 100644
--- a/libpod/events/journal_linux.go
+++ b/libpod/events/journal_linux.go
@@ -56,6 +56,9 @@ func (e EventJournalD) Write(ee Event) error {
}
m["PODMAN_LABELS"] = string(b)
}
+ case Network:
+ m["PODMAN_ID"] = ee.ID
+ m["PODMAN_NETWORK_NAME"] = ee.Network
case Volume:
m["PODMAN_NAME"] = ee.Name
}
@@ -197,6 +200,9 @@ func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { /
newEvent.Details = Details{Attributes: labels}
}
}
+ case Network:
+ newEvent.ID = entry.Fields["PODMAN_ID"]
+ newEvent.Network = entry.Fields["PODMAN_NETWORK_NAME"]
case Image:
newEvent.ID = entry.Fields["PODMAN_ID"]
}
diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go
index b70102450..57e38b815 100644
--- a/libpod/events/logfile.go
+++ b/libpod/events/logfile.go
@@ -76,7 +76,7 @@ func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error {
return err
}
switch event.Type {
- case Image, Volume, Pod, System, Container:
+ case Image, Volume, Pod, System, Container, Network:
// no-op
default:
return errors.Errorf("event type %s is not valid in %s", event.Type.String(), e.options.LogFilePath)
diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go
index 3882e095a..8dce7c9fe 100644
--- a/libpod/networking_linux.go
+++ b/libpod/networking_linux.go
@@ -21,6 +21,7 @@ import (
cnitypes "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/podman/v2/libpod/define"
+ "github.com/containers/podman/v2/libpod/events"
"github.com/containers/podman/v2/libpod/network"
"github.com/containers/podman/v2/pkg/errorhandling"
"github.com/containers/podman/v2/pkg/netns"
@@ -34,16 +35,16 @@ import (
)
// Get an OCICNI network config
-func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, ports []ocicni.PortMapping, staticIP net.IP, staticMAC net.HardwareAddr) ocicni.PodNetwork {
+func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, ports []ocicni.PortMapping, staticIP net.IP, staticMAC net.HardwareAddr, netDescriptions ContainerNetworkDescriptions) ocicni.PodNetwork {
var networkKey string
if len(networks) > 0 {
- // This is inconsistent for >1 network, but it's probably the
+ // This is inconsistent for >1 ctrNetwork, but it's probably the
// best we can do.
networkKey = networks[0]
} else {
networkKey = r.netPlugin.GetDefaultNetworkName()
}
- network := ocicni.PodNetwork{
+ ctrNetwork := ocicni.PodNetwork{
Name: name,
Namespace: name, // TODO is there something else we should put here? We don't know about Kube namespaces
ID: id,
@@ -55,9 +56,12 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port
// If we have extra networks, add them
if len(networks) > 0 {
- network.Networks = make([]ocicni.NetAttachment, len(networks))
+ ctrNetwork.Networks = make([]ocicni.NetAttachment, len(networks))
for i, netName := range networks {
- network.Networks[i].Name = netName
+ ctrNetwork.Networks[i].Name = netName
+ if eth, exists := netDescriptions.getInterfaceByName(netName); exists {
+ ctrNetwork.Networks[i].Ifname = eth
+ }
}
}
@@ -66,8 +70,8 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port
// it's just the default.
if len(networks) == 0 {
// If len(networks) == 0 this is guaranteed to be the
- // default network.
- network.Networks = []ocicni.NetAttachment{{Name: networkKey}}
+ // default ctrNetwork.
+ ctrNetwork.Networks = []ocicni.NetAttachment{{Name: networkKey}}
}
var rt ocicni.RuntimeConfig = ocicni.RuntimeConfig{PortMappings: ports}
if staticIP != nil {
@@ -76,12 +80,12 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port
if staticMAC != nil {
rt.MAC = staticMAC.String()
}
- network.RuntimeConfig = map[string]ocicni.RuntimeConfig{
+ ctrNetwork.RuntimeConfig = map[string]ocicni.RuntimeConfig{
networkKey: rt,
}
}
- return network
+ return ctrNetwork
}
// Create and configure a new network namespace for a container
@@ -110,7 +114,12 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) ([]*cnitypes.Re
if err != nil {
return nil, err
}
- podNetwork := r.getPodNetwork(ctr.ID(), podName, ctrNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC)
+
+ // Update container map of interface descriptions
+ if err := ctr.setupNetworkDescriptions(networks); err != nil {
+ return nil, err
+ }
+ podNetwork := r.getPodNetwork(ctr.ID(), podName, ctrNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ctr.state.NetInterfaceDescriptions)
aliases, err := ctr.runtime.state.GetAllNetworkAliases(ctr)
if err != nil {
return nil, err
@@ -760,7 +769,7 @@ func (r *Runtime) teardownNetNS(ctr *Container) error {
requestedMAC = ctr.config.StaticMAC
}
- podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC)
+ podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ContainerNetworkDescriptions{})
if err := r.netPlugin.TearDownPod(podNetwork); err != nil {
return errors.Wrapf(err, "error tearing down CNI namespace configuration for container %s", ctr.ID())
@@ -934,6 +943,29 @@ func (c *Container) getContainerNetworkInfo() (*define.InspectNetworkSettings, e
return settings, nil
}
+// setupNetworkDescriptions adds networks and eth values to the container's
+// network descriptions
+func (c *Container) setupNetworkDescriptions(networks []string) error {
+ // if the map is nil and we have networks
+ if c.state.NetInterfaceDescriptions == nil && len(networks) > 0 {
+ c.state.NetInterfaceDescriptions = make(ContainerNetworkDescriptions)
+ }
+ origLen := len(c.state.NetInterfaceDescriptions)
+ for _, n := range networks {
+ // if the network is not in the map, add it
+ if _, exists := c.state.NetInterfaceDescriptions[n]; !exists {
+ c.state.NetInterfaceDescriptions.add(n)
+ }
+ }
+ // if the map changed, we need to save the container state
+ if origLen != len(c.state.NetInterfaceDescriptions) {
+ if err := c.save(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// resultToBasicNetworkConfig produces an InspectBasicNetworkConfig from a CNI
// result
func resultToBasicNetworkConfig(result *cnitypes.Result) (define.InspectBasicNetworkConfig, error) {
@@ -984,19 +1016,14 @@ func (w *logrusDebugWriter) Write(p []byte) (int, error) {
return len(p), nil
}
-// DisconnectContainerFromNetwork removes a container from its CNI network
-func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force bool) error {
- ctr, err := r.LookupContainer(nameOrID)
+// NetworkDisconnect removes a container from the network
+func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) error {
+ networks, err := c.networksByNameIndex()
if err != nil {
return err
}
- networks, err := ctr.networksByNameIndex()
- if err != nil {
- return err
- }
-
- exists, err := network.Exists(r.config, netName)
+ exists, err := network.Exists(c.runtime.config, netName)
if err != nil {
return err
}
@@ -1009,48 +1036,48 @@ func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force
return errors.Errorf("container %s is not connected to network %s", nameOrID, netName)
}
- ctr.lock.Lock()
- defer ctr.lock.Unlock()
- if err := ctr.syncContainer(); err != nil {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ if err := c.syncContainer(); err != nil {
return err
}
- podConfig := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), []string{netName}, ctr.config.PortMappings, nil, nil)
- if err := r.netPlugin.TearDownPod(podConfig); err != nil {
+ if c.state.State != define.ContainerStateRunning {
+ return errors.Wrapf(define.ErrCtrStateInvalid, "cannot disconnect container %s from networks as it is not running", nameOrID)
+ }
+ if c.state.NetNS == nil {
+ return errors.Wrapf(define.ErrNoNetwork, "unable to disconnect %s from %s", nameOrID, netName)
+ }
+ podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions)
+ if err := c.runtime.netPlugin.TearDownPod(podConfig); err != nil {
return err
}
- if err := r.state.NetworkDisconnect(ctr, netName); err != nil {
+ if err := c.runtime.state.NetworkDisconnect(c, netName); err != nil {
return err
}
// update network status
- networkStatus := ctr.state.NetworkStatus
- // if len is one and we confirmed earlier that the container is in
- // fact connected to the network, then just return an empty slice
- if len(networkStatus) == 1 {
- ctr.state.NetworkStatus = make([]*cnitypes.Result, 0)
- } else {
- // clip out the index of the network
- networkStatus[len(networkStatus)-1], networkStatus[index] = networkStatus[index], networkStatus[len(networkStatus)-1]
- // shorten the slice by one
- ctr.state.NetworkStatus = networkStatus[:len(networkStatus)-1]
+ networkStatus := c.state.NetworkStatus
+ // clip out the index of the network
+ tmpNetworkStatus := make([]*cnitypes.Result, len(networkStatus)-1)
+ for k, v := range networkStatus {
+ if index != k {
+ tmpNetworkStatus = append(tmpNetworkStatus, v)
+ }
}
- return nil
+ c.state.NetworkStatus = tmpNetworkStatus
+ c.newNetworkEvent(events.NetworkDisconnect, netName)
+ return c.save()
}
-// ConnectContainerToNetwork connects a container to a CNI network
-func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []string) error {
- ctr, err := r.LookupContainer(nameOrID)
+// ConnnectNetwork connects a container to a given network
+func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) error {
+ networks, err := c.networksByNameIndex()
if err != nil {
return err
}
- networks, err := ctr.networksByNameIndex()
- if err != nil {
- return err
- }
-
- exists, err := network.Exists(r.config, netName)
+ exists, err := network.Exists(c.runtime.config, netName)
if err != nil {
return err
}
@@ -1058,25 +1085,34 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []
return errors.Wrap(define.ErrNoSuchNetwork, netName)
}
- _, nameExists := networks[netName]
- if !nameExists && len(networks) > 0 {
- return errors.Errorf("container %s is not connected to network %s", nameOrID, netName)
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ if err := c.syncContainer(); err != nil {
+ return err
}
- ctr.lock.Lock()
- defer ctr.lock.Unlock()
- if err := ctr.syncContainer(); err != nil {
+ if c.state.State != define.ContainerStateRunning {
+ return errors.Wrapf(define.ErrCtrStateInvalid, "cannot connect container %s to networks as it is not running", nameOrID)
+ }
+ if c.state.NetNS == nil {
+ return errors.Wrapf(define.ErrNoNetwork, "unable to connect %s to %s", nameOrID, netName)
+ }
+ if err := c.runtime.state.NetworkConnect(c, netName, aliases); err != nil {
return err
}
- if err := r.state.NetworkConnect(ctr, netName, aliases); err != nil {
+ ctrNetworks, err := c.networks()
+ if err != nil {
return err
}
-
- podConfig := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), []string{netName}, ctr.config.PortMappings, nil, nil)
+ // Update network descriptions
+ if err := c.setupNetworkDescriptions(ctrNetworks); err != nil {
+ return err
+ }
+ podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions)
podConfig.Aliases = make(map[string][]string, 1)
podConfig.Aliases[netName] = aliases
- results, err := r.netPlugin.SetUpPod(podConfig)
+ results, err := c.runtime.netPlugin.SetUpPod(podConfig)
if err != nil {
return err
}
@@ -1094,11 +1130,11 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []
}
// update network status
- networkStatus := ctr.state.NetworkStatus
+ networkStatus := c.state.NetworkStatus
// if len is one and we confirmed earlier that the container is in
// fact connected to the network, then just return an empty slice
if len(networkStatus) == 0 {
- ctr.state.NetworkStatus = append(ctr.state.NetworkStatus, networkResults...)
+ c.state.NetworkStatus = append(c.state.NetworkStatus, networkResults...)
} else {
// build a list of network names so we can sort and
// get the new name's index
@@ -1117,5 +1153,30 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []
copy(networkStatus[index+1:], networkStatus[index:])
networkStatus[index] = networkResults[0]
}
- return nil
+ c.newNetworkEvent(events.NetworkConnect, netName)
+ return c.save()
+}
+
+// DisconnectContainerFromNetwork removes a container from its CNI network
+func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force bool) error {
+ if rootless.IsRootless() {
+ return errors.New("network connect is not enabled for rootless containers")
+ }
+ ctr, err := r.LookupContainer(nameOrID)
+ if err != nil {
+ return err
+ }
+ return ctr.NetworkDisconnect(nameOrID, netName, force)
+}
+
+// ConnectContainerToNetwork connects a container to a CNI network
+func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []string) error {
+ if rootless.IsRootless() {
+ return errors.New("network disconnect is not enabled for rootless containers")
+ }
+ ctr, err := r.LookupContainer(nameOrID)
+ if err != nil {
+ return err
+ }
+ return ctr.NetworkConnect(nameOrID, netName, aliases)
}
diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go
index 78e525f1f..f1578f829 100644
--- a/pkg/api/handlers/libpod/networks.go
+++ b/pkg/api/handlers/libpod/networks.go
@@ -131,3 +131,29 @@ func InspectNetwork(w http.ResponseWriter, r *http.Request) {
}
utils.WriteResponse(w, http.StatusOK, reports)
}
+
+// Connect adds a container to a network
+func Connect(w http.ResponseWriter, r *http.Request) {
+ runtime := r.Context().Value("runtime").(*libpod.Runtime)
+
+ var netConnect entities.NetworkConnectOptions
+ if err := json.NewDecoder(r.Body).Decode(&netConnect); err != nil {
+ utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()"))
+ return
+ }
+ name := utils.GetName(r)
+ err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netConnect.Aliases)
+ if err != nil {
+ if errors.Cause(err) == define.ErrNoSuchCtr {
+ utils.ContainerNotFound(w, netConnect.Container, err)
+ return
+ }
+ if errors.Cause(err) == define.ErrNoSuchNetwork {
+ utils.Error(w, "network not found", http.StatusNotFound, err)
+ return
+ }
+ utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
+ return
+ }
+ utils.WriteResponse(w, http.StatusOK, "OK")
+}
diff --git a/pkg/api/server/register_networks.go b/pkg/api/server/register_networks.go
index 6222006e5..ea169cbdf 100644
--- a/pkg/api/server/register_networks.go
+++ b/pkg/api/server/register_networks.go
@@ -253,5 +253,59 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error {
// 500:
// $ref: "#/responses/InternalError"
r.HandleFunc(VersionedPath("/libpod/networks/create"), s.APIHandler(libpod.CreateNetwork)).Methods(http.MethodPost)
+ // swagger:operation POST /libpod/networks/{name}/connect libpod libpodConnectNetwork
+ // ---
+ // tags:
+ // - networks
+ // summary: Connect container to network
+ // description: Connect a container to a network.
+ // produces:
+ // - application/json
+ // parameters:
+ // - in: path
+ // name: name
+ // type: string
+ // required: true
+ // description: the name of the network
+ // - in: body
+ // name: create
+ // description: attributes for connecting a container to a network
+ // schema:
+ // $ref: "#/definitions/NetworkConnectRequest"
+ // responses:
+ // 200:
+ // description: OK
+ // 404:
+ // $ref: "#/responses/NoSuchNetwork"
+ // 500:
+ // $ref: "#/responses/InternalError"
+ r.HandleFunc(VersionedPath("/libpod/networks/{name}/connect"), s.APIHandler(libpod.Connect)).Methods(http.MethodPost)
+ // swagger:operation POST /libpod/networks/{name}/disconnect libpod libpodDisconnectNetwork
+ // ---
+ // tags:
+ // - networks
+ // summary: Disconnect container from network
+ // description: Disconnect a container from a network.
+ // produces:
+ // - application/json
+ // parameters:
+ // - in: path
+ // name: name
+ // type: string
+ // required: true
+ // description: the name of the network
+ // - in: body
+ // name: create
+ // description: attributes for disconnecting a container from a network
+ // schema:
+ // $ref: "#/definitions/NetworkDisconnectRequest"
+ // responses:
+ // 200:
+ // description: OK
+ // 404:
+ // $ref: "#/responses/NoSuchNetwork"
+ // 500:
+ // $ref: "#/responses/InternalError"
+ r.HandleFunc(VersionedPath("/libpod/networks/{name}/disconnect"), s.APIHandler(compat.Disconnect)).Methods(http.MethodPost)
return nil
}
diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go
index 151d15d3e..1d4be8a4c 100644
--- a/pkg/bindings/network/network.go
+++ b/pkg/bindings/network/network.go
@@ -88,3 +88,41 @@ func List(ctx context.Context, options entities.NetworkListOptions) ([]*entities
}
return netList, response.Process(&netList)
}
+
+// Disconnect removes a container from a given network
+func Disconnect(ctx context.Context, networkName string, options entities.NetworkDisconnectOptions) error {
+ conn, err := bindings.GetClient(ctx)
+ if err != nil {
+ return err
+ }
+ params := url.Values{}
+ body, err := jsoniter.MarshalToString(options)
+ if err != nil {
+ return err
+ }
+ stringReader := strings.NewReader(body)
+ response, err := conn.DoRequest(stringReader, http.MethodPost, "/networks/%s/disconnect", params, nil, networkName)
+ if err != nil {
+ return err
+ }
+ return response.Process(nil)
+}
+
+// Connect adds a container to a network
+func Connect(ctx context.Context, networkName string, options entities.NetworkConnectOptions) error {
+ conn, err := bindings.GetClient(ctx)
+ if err != nil {
+ return err
+ }
+ params := url.Values{}
+ body, err := jsoniter.MarshalToString(options)
+ if err != nil {
+ return err
+ }
+ stringReader := strings.NewReader(body)
+ response, err := conn.DoRequest(stringReader, http.MethodPost, "/networks/%s/connect", params, nil, networkName)
+ if err != nil {
+ return err
+ }
+ return response.Process(nil)
+}
diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go
index 8ab72dbd8..b051d3eec 100644
--- a/pkg/domain/entities/engine_container.go
+++ b/pkg/domain/entities/engine_container.go
@@ -50,7 +50,9 @@ type ContainerEngine interface {
SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error)
HealthCheckRun(ctx context.Context, nameOrID string, options HealthCheckOptions) (*define.HealthCheckResults, error)
Info(ctx context.Context) (*define.Info, error)
+ NetworkConnect(ctx context.Context, networkname string, options NetworkConnectOptions) error
NetworkCreate(ctx context.Context, name string, options NetworkCreateOptions) (*NetworkCreateReport, error)
+ NetworkDisconnect(ctx context.Context, networkname string, options NetworkDisconnectOptions) error
NetworkInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]NetworkInspectReport, []error, error)
NetworkList(ctx context.Context, options NetworkListOptions) ([]*NetworkListReport, error)
NetworkRm(ctx context.Context, namesOrIds []string, options NetworkRmOptions) ([]*NetworkRmReport, error)
diff --git a/pkg/domain/entities/network.go b/pkg/domain/entities/network.go
index 3cc970531..86c2e1bcd 100644
--- a/pkg/domain/entities/network.go
+++ b/pkg/domain/entities/network.go
@@ -49,3 +49,17 @@ type NetworkCreateOptions struct {
type NetworkCreateReport struct {
Filename string
}
+
+// NetworkDisconnectOptions describes options for disconnecting
+// containers from networks
+type NetworkDisconnectOptions struct {
+ Container string
+ Force bool
+}
+
+// NetworkConnectOptions describes options for connecting
+// a container to a network
+type NetworkConnectOptions struct {
+ Aliases []string
+ Container string
+}
diff --git a/pkg/domain/infra/abi/network.go b/pkg/domain/infra/abi/network.go
index 06941f8d0..25ea151e7 100644
--- a/pkg/domain/infra/abi/network.go
+++ b/pkg/domain/infra/abi/network.go
@@ -138,3 +138,12 @@ func ifPassesFilterTest(netconf *libcni.NetworkConfigList, filter []string) bool
}
return result
}
+
+// NetworkDisconnect removes a container from a given network
+func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname string, options entities.NetworkDisconnectOptions) error {
+ return ic.Libpod.DisconnectContainerFromNetwork(options.Container, networkname, options.Force)
+}
+
+func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, options entities.NetworkConnectOptions) error {
+ return ic.Libpod.ConnectContainerToNetwork(options.Container, networkname, options.Aliases)
+}
diff --git a/pkg/domain/infra/tunnel/network.go b/pkg/domain/infra/tunnel/network.go
index 15527e02c..10ae03045 100644
--- a/pkg/domain/infra/tunnel/network.go
+++ b/pkg/domain/infra/tunnel/network.go
@@ -55,3 +55,13 @@ func (ic *ContainerEngine) NetworkRm(ctx context.Context, namesOrIds []string, o
func (ic *ContainerEngine) NetworkCreate(ctx context.Context, name string, options entities.NetworkCreateOptions) (*entities.NetworkCreateReport, error) {
return network.Create(ic.ClientCxt, options, &name)
}
+
+// NetworkDisconnect removes a container from a given network
+func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname string, options entities.NetworkDisconnectOptions) error {
+ return network.Disconnect(ic.ClientCxt, networkname, options)
+}
+
+// NetworkConnect removes a container from a given network
+func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, options entities.NetworkConnectOptions) error {
+ return network.Connect(ic.ClientCxt, networkname, options)
+}
diff --git a/test/e2e/network_test.go b/test/e2e/network_test.go
index b010010f0..c6593df34 100644
--- a/test/e2e/network_test.go
+++ b/test/e2e/network_test.go
@@ -341,4 +341,157 @@ var _ = Describe("Podman network", func() {
c3.WaitWithDefaultTimeout()
Expect(c3.ExitCode()).To(BeZero())
})
+
+ It("bad network name in disconnect should result in error", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ dis := podmanTest.Podman([]string{"network", "disconnect", "foobar", "test"})
+ dis.WaitWithDefaultTimeout()
+ Expect(dis.ExitCode()).ToNot(BeZero())
+
+ })
+
+ It("bad container name in network disconnect should result in error", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ netName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session := podmanTest.Podman([]string{"network", "create", netName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(netName)
+
+ dis := podmanTest.Podman([]string{"network", "disconnect", netName, "foobar"})
+ dis.WaitWithDefaultTimeout()
+ Expect(dis.ExitCode()).ToNot(BeZero())
+
+ })
+
+ It("podman network disconnect with invalid container state should result in error", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ netName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session := podmanTest.Podman([]string{"network", "create", netName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(netName)
+
+ ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"})
+ ctr.WaitWithDefaultTimeout()
+ Expect(ctr.ExitCode()).To(BeZero())
+
+ dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"})
+ dis.WaitWithDefaultTimeout()
+ Expect(dis.ExitCode()).ToNot(BeZero())
+ })
+
+ It("podman network disconnect", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ netName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session := podmanTest.Podman([]string{"network", "create", netName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(netName)
+
+ ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"})
+ ctr.WaitWithDefaultTimeout()
+ Expect(ctr.ExitCode()).To(BeZero())
+
+ exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"})
+ exec.WaitWithDefaultTimeout()
+ Expect(exec.ExitCode()).To(BeZero())
+
+ dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"})
+ dis.WaitWithDefaultTimeout()
+ Expect(dis.ExitCode()).To(BeZero())
+
+ exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"})
+ exec.WaitWithDefaultTimeout()
+ Expect(exec.ExitCode()).ToNot(BeZero())
+ })
+
+ It("bad network name in connect should result in error", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ dis := podmanTest.Podman([]string{"network", "connect", "foobar", "test"})
+ dis.WaitWithDefaultTimeout()
+ Expect(dis.ExitCode()).ToNot(BeZero())
+
+ })
+
+ It("bad container name in network connect should result in error", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ netName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session := podmanTest.Podman([]string{"network", "create", netName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(netName)
+
+ dis := podmanTest.Podman([]string{"network", "connect", netName, "foobar"})
+ dis.WaitWithDefaultTimeout()
+ Expect(dis.ExitCode()).ToNot(BeZero())
+
+ })
+
+ It("podman connect on a container that already is connected to the network should error", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ netName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session := podmanTest.Podman([]string{"network", "create", netName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(netName)
+
+ ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"})
+ ctr.WaitWithDefaultTimeout()
+ Expect(ctr.ExitCode()).To(BeZero())
+
+ con := podmanTest.Podman([]string{"network", "connect", netName, "test"})
+ con.WaitWithDefaultTimeout()
+ Expect(con.ExitCode()).ToNot(BeZero())
+ })
+
+ It("podman network connect with invalid container state should result in error", func() {
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ netName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session := podmanTest.Podman([]string{"network", "create", netName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(netName)
+
+ ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"})
+ ctr.WaitWithDefaultTimeout()
+ Expect(ctr.ExitCode()).To(BeZero())
+
+ dis := podmanTest.Podman([]string{"network", "connect", netName, "test"})
+ dis.WaitWithDefaultTimeout()
+ Expect(dis.ExitCode()).ToNot(BeZero())
+ })
+
+ It("podman network connect", func() {
+ SkipIfRemote("This requires a pending PR to be merged before it will work")
+ SkipIfRootless("network connect and disconnect are only rootfull")
+ netName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session := podmanTest.Podman([]string{"network", "create", netName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(netName)
+
+ ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"})
+ ctr.WaitWithDefaultTimeout()
+ Expect(ctr.ExitCode()).To(BeZero())
+
+ exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"})
+ exec.WaitWithDefaultTimeout()
+ Expect(exec.ExitCode()).To(BeZero())
+
+ // Create a second network
+ newNetName := "aliasTest" + stringid.GenerateNonCryptoID()
+ session = podmanTest.Podman([]string{"network", "create", newNetName})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(BeZero())
+ defer podmanTest.removeCNINetwork(newNetName)
+
+ connect := podmanTest.Podman([]string{"network", "connect", newNetName, "test"})
+ connect.WaitWithDefaultTimeout()
+ Expect(connect.ExitCode()).To(BeZero())
+
+ exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth1"})
+ exec.WaitWithDefaultTimeout()
+ Expect(exec.ExitCode()).To(BeZero())
+ })
})