mirror of
https://github.com/containers/podman
synced 2024-10-19 08:44:11 +00:00
Add support for multiple CNI networks in podman inspect
When inspecting containers, info on CNI networks added to the container by name (e.g. --net=name1) should be displayed separately from the configuration of the default network, in a separate map called Networks. This patch adds this separation, improving our Docker compatibility and also adding the ability to see if a container has more than one IPv4 and IPv6 address and more than one MAC address. Fixes #4907 Signed-off-by: Matthew Heon <matthew.heon@pm.me>
This commit is contained in:
parent
3d37dc639d
commit
f9fc9a7b7b
|
@ -606,11 +606,45 @@ type InspectContainerState struct {
|
|||
Healthcheck HealthCheckResults `json:"Healthcheck,omitempty"`
|
||||
}
|
||||
|
||||
// InspectBasicNetworkConfig holds basic configuration information (e.g. IP
|
||||
// addresses, MAC address, subnet masks, etc) that are common for all networks
|
||||
// (both additional and main).
|
||||
type InspectBasicNetworkConfig struct {
|
||||
// EndpointID is unused, maintained exclusively for compatibility.
|
||||
EndpointID string `json:"EndpointID"`
|
||||
// Gateway is the IP address of the gateway this network will use.
|
||||
Gateway string `json:"Gateway"`
|
||||
// IPAddress is the IP address for this network.
|
||||
IPAddress string `json:"IPAddress"`
|
||||
// IPPrefixLen is the length of the subnet mask of this network.
|
||||
IPPrefixLen int `json:"IPPrefixLen"`
|
||||
// SecondaryIPAddresses is a list of extra IP Addresses that the
|
||||
// container has been assigned in this network.
|
||||
SecondaryIPAddresses []string `json:"SecondaryIPAddresses,omitempty"`
|
||||
// IPv6Gateway is the IPv6 gateway this network will use.
|
||||
IPv6Gateway string `json:"IPv6Gateway"`
|
||||
// GlobalIPv6Address is the global-scope IPv6 Address for this network.
|
||||
GlobalIPv6Address string `json:"GlobalIPv6Address"`
|
||||
// GlobalIPv6PrefixLen is the length of the subnet mask of this network.
|
||||
GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"`
|
||||
// SecondaryIPv6Addresses is a list of extra IPv6 Addresses that the
|
||||
// container has been assigned in this networ.
|
||||
SecondaryIPv6Addresses []string `json:"SecondaryIPv6Addresses,omitempty"`
|
||||
// MacAddress is the MAC address for the interface in this network.
|
||||
MacAddress string `json:"MacAddress"`
|
||||
// AdditionalMacAddresses is a set of additional MAC Addresses beyond
|
||||
// the first. CNI may configure more than one interface for a single
|
||||
// network, which can cause this.
|
||||
AdditionalMacAddresses []string `json:"AdditionalMACAddresses,omitempty"`
|
||||
}
|
||||
|
||||
// InspectNetworkSettings holds information about the network settings of the
|
||||
// container.
|
||||
// Many fields are maintained only for compatibility with `docker inspect` and
|
||||
// are unused within Libpod.
|
||||
type InspectNetworkSettings struct {
|
||||
InspectBasicNetworkConfig
|
||||
|
||||
Bridge string `json:"Bridge"`
|
||||
SandboxID string `json:"SandboxID"`
|
||||
HairpinMode bool `json:"HairpinMode"`
|
||||
|
@ -618,16 +652,30 @@ type InspectNetworkSettings struct {
|
|||
LinkLocalIPv6PrefixLen int `json:"LinkLocalIPv6PrefixLen"`
|
||||
Ports []ocicni.PortMapping `json:"Ports"`
|
||||
SandboxKey string `json:"SandboxKey"`
|
||||
SecondaryIPAddresses []string `json:"SecondaryIPAddresses"`
|
||||
SecondaryIPv6Addresses []string `json:"SecondaryIPv6Addresses"`
|
||||
EndpointID string `json:"EndpointID"`
|
||||
Gateway string `json:"Gateway"`
|
||||
GlobalIPv6Address string `json:"GlobalIPv6Address"`
|
||||
GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"`
|
||||
IPAddress string `json:"IPAddress"`
|
||||
IPPrefixLen int `json:"IPPrefixLen"`
|
||||
IPv6Gateway string `json:"IPv6Gateway"`
|
||||
MacAddress string `json:"MacAddress"`
|
||||
// Networks contains information on non-default CNI networks this
|
||||
// container has joined.
|
||||
// It is a map of network name to network information.
|
||||
Networks map[string]*InspectAdditionalNetwork `json:"Networks,omitempty"`
|
||||
}
|
||||
|
||||
// InspectAdditionalNetwork holds information about non-default CNI networks the
|
||||
// container has been connected to.
|
||||
// As with InspectNetworkSettings, many fields are unused and maintained only
|
||||
// for compatibility with Docker.
|
||||
type InspectAdditionalNetwork struct {
|
||||
InspectBasicNetworkConfig
|
||||
|
||||
// Name of the network we're connecting to.
|
||||
NetworkID string `json:"NetworkID,omitempty"`
|
||||
// DriverOpts is presently unused and maintained exclusively for
|
||||
// compatibility.
|
||||
DriverOpts map[string]string `json:"DriverOpts"`
|
||||
// IPAMConfig is presently unused and maintained exlusively for
|
||||
// compabitility.
|
||||
IPAMConfig map[string]string `json:"IPAMConfig"`
|
||||
// Links is presently unused and maintained exclusively for
|
||||
// compatibility.
|
||||
Links []string `json:"Links"`
|
||||
}
|
||||
|
||||
// inspectLocked inspects a container for low-level information.
|
||||
|
@ -754,27 +802,7 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data)
|
|||
GraphDriver: driverData,
|
||||
Mounts: inspectMounts,
|
||||
Dependencies: c.Dependencies(),
|
||||
NetworkSettings: &InspectNetworkSettings{
|
||||
Bridge: "", // TODO
|
||||
SandboxID: "", // TODO - is this even relevant?
|
||||
HairpinMode: false, // TODO
|
||||
LinkLocalIPv6Address: "", // TODO - do we even support IPv6?
|
||||
LinkLocalIPv6PrefixLen: 0, // TODO - do we even support IPv6?
|
||||
|
||||
Ports: []ocicni.PortMapping{}, // TODO - maybe worth it to put this in Docker format?
|
||||
SandboxKey: "", // Network namespace path
|
||||
SecondaryIPAddresses: nil, // TODO - do we support this?
|
||||
SecondaryIPv6Addresses: nil, // TODO - do we support this?
|
||||
EndpointID: "", // TODO - is this even relevant?
|
||||
Gateway: "", // TODO
|
||||
GlobalIPv6Address: "",
|
||||
GlobalIPv6PrefixLen: 0,
|
||||
IPAddress: "",
|
||||
IPPrefixLen: 0,
|
||||
IPv6Gateway: "",
|
||||
MacAddress: "", // TODO
|
||||
},
|
||||
IsInfra: c.IsInfra(),
|
||||
IsInfra: c.IsInfra(),
|
||||
}
|
||||
|
||||
if c.state.ConfigPath != "" {
|
||||
|
@ -792,13 +820,11 @@ func (c *Container) getContainerInspectData(size bool, driverData *driver.Data)
|
|||
}
|
||||
}
|
||||
|
||||
// Copy port mappings into network settings
|
||||
if config.PortMappings != nil {
|
||||
data.NetworkSettings.Ports = config.PortMappings
|
||||
networkConfig, err := c.getContainerNetworkInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get information on the container's network namespace (if present)
|
||||
data = c.getContainerNetworkInfo(data)
|
||||
data.NetworkSettings = networkConfig
|
||||
|
||||
inspectConfig, err := c.generateInspectContainerConfig(ctrSpec)
|
||||
if err != nil {
|
||||
|
|
|
@ -12,13 +12,13 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
cnitypes "github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containers/libpod/libpod/define"
|
||||
"github.com/containers/libpod/pkg/errorhandling"
|
||||
"github.com/containers/libpod/pkg/netns"
|
||||
"github.com/containers/libpod/pkg/rootless"
|
||||
|
@ -556,37 +556,105 @@ func getContainerNetIO(ctr *Container) (*netlink.LinkStatistics, error) {
|
|||
return netStats, err
|
||||
}
|
||||
|
||||
func (c *Container) getContainerNetworkInfo(data *InspectContainerData) *InspectContainerData {
|
||||
if c.state.NetNS != nil && len(c.state.NetworkStatus) > 0 {
|
||||
// Report network settings from the first pod network
|
||||
result := c.state.NetworkStatus[0]
|
||||
// Go through our IP addresses
|
||||
for _, ctrIP := range result.IPs {
|
||||
ipWithMask := ctrIP.Address.String()
|
||||
splitIP := strings.Split(ipWithMask, "/")
|
||||
mask, _ := strconv.Atoi(splitIP[1])
|
||||
if ctrIP.Version == "4" {
|
||||
data.NetworkSettings.IPAddress = splitIP[0]
|
||||
data.NetworkSettings.IPPrefixLen = mask
|
||||
data.NetworkSettings.Gateway = ctrIP.Gateway.String()
|
||||
} else {
|
||||
data.NetworkSettings.GlobalIPv6Address = splitIP[0]
|
||||
data.NetworkSettings.GlobalIPv6PrefixLen = mask
|
||||
data.NetworkSettings.IPv6Gateway = ctrIP.Gateway.String()
|
||||
}
|
||||
// Produce an InspectNetworkSettings containing information on the container
|
||||
// network.
|
||||
func (c *Container) getContainerNetworkInfo() (*InspectNetworkSettings, error) {
|
||||
settings := new(InspectNetworkSettings)
|
||||
settings.Ports = []ocicni.PortMapping{}
|
||||
if c.config.PortMappings != nil {
|
||||
// TODO: This may not be safe.
|
||||
settings.Ports = c.config.PortMappings
|
||||
}
|
||||
|
||||
// We can't do more if the network is down.
|
||||
if c.state.NetNS == nil {
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// Set network namespace path
|
||||
settings.SandboxKey = c.state.NetNS.Path()
|
||||
|
||||
// If this is empty, we're probably slirp4netns
|
||||
if len(c.state.NetworkStatus) == 0 {
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// If we have CNI networks - handle that here
|
||||
if len(c.config.Networks) > 0 {
|
||||
if len(c.config.Networks) != len(c.state.NetworkStatus) {
|
||||
return nil, errors.Wrapf(define.ErrInternal, "network inspection mismatch: asked to join %d CNI networks but have information on %d networks", len(c.config.Networks), len(c.state.NetworkStatus))
|
||||
}
|
||||
|
||||
// Set network namespace path
|
||||
data.NetworkSettings.SandboxKey = c.state.NetNS.Path()
|
||||
settings.Networks = make(map[string]*InspectAdditionalNetwork)
|
||||
|
||||
// Set MAC address of interface linked with network namespace path
|
||||
for _, i := range result.Interfaces {
|
||||
if i.Sandbox == data.NetworkSettings.SandboxKey {
|
||||
data.NetworkSettings.MacAddress = i.Mac
|
||||
// CNI results should be in the same order as the list of
|
||||
// networks we pass into CNI.
|
||||
for index, name := range c.config.Networks {
|
||||
cniResult := c.state.NetworkStatus[index]
|
||||
addedNet := new(InspectAdditionalNetwork)
|
||||
addedNet.NetworkID = name
|
||||
|
||||
basicConfig, err := resultToBasicNetworkConfig(cniResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addedNet.InspectBasicNetworkConfig = basicConfig
|
||||
|
||||
settings.Networks[name] = addedNet
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// If not joining networks, we should have at most 1 result
|
||||
if len(c.state.NetworkStatus) > 1 {
|
||||
return nil, errors.Wrapf(define.ErrInternal, "should have at most 1 CNI result if not joining networks, instead got %d", len(c.state.NetworkStatus))
|
||||
}
|
||||
|
||||
if len(c.state.NetworkStatus) == 1 {
|
||||
basicConfig, err := resultToBasicNetworkConfig(c.state.NetworkStatus[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings.InspectBasicNetworkConfig = basicConfig
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// resultToBasicNetworkConfig produces an InspectBasicNetworkConfig from a CNI
|
||||
// result
|
||||
func resultToBasicNetworkConfig(result *cnitypes.Result) (InspectBasicNetworkConfig, error) {
|
||||
config := InspectBasicNetworkConfig{}
|
||||
|
||||
for _, ctrIP := range result.IPs {
|
||||
size, _ := ctrIP.Address.Mask.Size()
|
||||
switch {
|
||||
case ctrIP.Version == "4" && config.IPAddress == "":
|
||||
config.IPAddress = ctrIP.Address.IP.String()
|
||||
config.IPPrefixLen = size
|
||||
config.Gateway = ctrIP.Gateway.String()
|
||||
if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface > 0 {
|
||||
config.MacAddress = result.Interfaces[*ctrIP.Interface].Mac
|
||||
}
|
||||
case ctrIP.Version == "4" && config.IPAddress != "":
|
||||
config.SecondaryIPAddresses = append(config.SecondaryIPAddresses, ctrIP.Address.String())
|
||||
if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface > 0 {
|
||||
config.AdditionalMacAddresses = append(config.AdditionalMacAddresses, result.Interfaces[*ctrIP.Interface].Mac)
|
||||
}
|
||||
case ctrIP.Version == "6" && config.IPAddress == "":
|
||||
config.GlobalIPv6Address = ctrIP.Address.IP.String()
|
||||
config.GlobalIPv6PrefixLen = size
|
||||
config.IPv6Gateway = ctrIP.Gateway.String()
|
||||
case ctrIP.Version == "6" && config.IPAddress != "":
|
||||
config.SecondaryIPv6Addresses = append(config.SecondaryIPv6Addresses, ctrIP.Address.String())
|
||||
default:
|
||||
return config, errors.Wrapf(define.ErrInternal, "unrecognized IP version %q", ctrIP.Version)
|
||||
}
|
||||
}
|
||||
return data
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type logrusDebugWriter struct {
|
||||
|
|
|
@ -20,6 +20,6 @@ func (r *Runtime) createNetNS(ctr *Container) (err error) {
|
|||
return define.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Container) getContainerNetworkInfo(data *InspectContainerData) *InspectContainerData {
|
||||
return nil
|
||||
func (c *Container) getContainerNetworkInfo() (*InspectNetworkSettings, error) {
|
||||
return nil, define.ErrNotImplemented
|
||||
}
|
||||
|
|
|
@ -4,13 +4,15 @@ package integration
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "github.com/containers/libpod/test/utils"
|
||||
"github.com/containers/storage/pkg/stringid"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func writeConf(conf []byte, confPath string) {
|
||||
|
@ -155,4 +157,76 @@ var _ = Describe("Podman network", func() {
|
|||
Expect(session.IsJSONOutputValid()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("podman inspect container single CNI network", func() {
|
||||
SkipIfRootless()
|
||||
netName := "testNetSingleCNI"
|
||||
network := podmanTest.Podman([]string{"network", "create", "--subnet", "10.50.50.0/24", netName})
|
||||
network.WaitWithDefaultTimeout()
|
||||
Expect(network.ExitCode()).To(BeZero())
|
||||
defer podmanTest.removeCNINetwork(netName)
|
||||
|
||||
ctrName := "testCtr"
|
||||
container := podmanTest.Podman([]string{"run", "-dt", "--network", netName, "--name", ctrName, ALPINE, "top"})
|
||||
container.WaitWithDefaultTimeout()
|
||||
Expect(container.ExitCode()).To(BeZero())
|
||||
|
||||
inspect := podmanTest.Podman([]string{"inspect", ctrName})
|
||||
inspect.WaitWithDefaultTimeout()
|
||||
Expect(inspect.ExitCode()).To(BeZero())
|
||||
conData := inspect.InspectContainerToJSON()
|
||||
Expect(len(conData)).To(Equal(1))
|
||||
Expect(len(conData[0].NetworkSettings.Networks)).To(Equal(1))
|
||||
net, ok := conData[0].NetworkSettings.Networks[netName]
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(net.NetworkID).To(Equal(netName))
|
||||
Expect(net.IPPrefixLen).To(Equal(24))
|
||||
Expect(strings.HasPrefix(net.IPAddress, "10.50.50.")).To(BeTrue())
|
||||
|
||||
// Necessary to ensure the CNI network is removed cleanly
|
||||
rmAll := podmanTest.Podman([]string{"rm", "-f", ctrName})
|
||||
rmAll.WaitWithDefaultTimeout()
|
||||
Expect(rmAll.ExitCode()).To(BeZero())
|
||||
})
|
||||
|
||||
It("podman inspect container two CNI networks", func() {
|
||||
SkipIfRootless()
|
||||
netName1 := "testNetTwoCNI1"
|
||||
network1 := podmanTest.Podman([]string{"network", "create", "--subnet", "10.50.51.0/25", netName1})
|
||||
network1.WaitWithDefaultTimeout()
|
||||
Expect(network1.ExitCode()).To(BeZero())
|
||||
defer podmanTest.removeCNINetwork(netName1)
|
||||
|
||||
netName2 := "testNetTwoCNI2"
|
||||
network2 := podmanTest.Podman([]string{"network", "create", "--subnet", "10.50.51.128/26", netName2})
|
||||
network2.WaitWithDefaultTimeout()
|
||||
Expect(network2.ExitCode()).To(BeZero())
|
||||
defer podmanTest.removeCNINetwork(netName2)
|
||||
|
||||
ctrName := "testCtr"
|
||||
container := podmanTest.Podman([]string{"run", "-dt", "--network", fmt.Sprintf("%s,%s", netName1, netName2), "--name", ctrName, ALPINE, "top"})
|
||||
container.WaitWithDefaultTimeout()
|
||||
Expect(container.ExitCode()).To(BeZero())
|
||||
|
||||
inspect := podmanTest.Podman([]string{"inspect", ctrName})
|
||||
inspect.WaitWithDefaultTimeout()
|
||||
Expect(inspect.ExitCode()).To(BeZero())
|
||||
conData := inspect.InspectContainerToJSON()
|
||||
Expect(len(conData)).To(Equal(1))
|
||||
Expect(len(conData[0].NetworkSettings.Networks)).To(Equal(2))
|
||||
net1, ok := conData[0].NetworkSettings.Networks[netName1]
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(net1.NetworkID).To(Equal(netName1))
|
||||
Expect(net1.IPPrefixLen).To(Equal(25))
|
||||
Expect(strings.HasPrefix(net1.IPAddress, "10.50.51.")).To(BeTrue())
|
||||
net2, ok := conData[0].NetworkSettings.Networks[netName2]
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(net2.NetworkID).To(Equal(netName2))
|
||||
Expect(net2.IPPrefixLen).To(Equal(26))
|
||||
Expect(strings.HasPrefix(net2.IPAddress, "10.50.51.")).To(BeTrue())
|
||||
|
||||
// Necessary to ensure the CNI network is removed cleanly
|
||||
rmAll := podmanTest.Podman([]string{"rm", "-f", ctrName})
|
||||
rmAll.WaitWithDefaultTimeout()
|
||||
Expect(rmAll.ExitCode()).To(BeZero())
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue