Merge pull request #11890 from Luap99/ports

libpod: deduplicate ports in db
This commit is contained in:
OpenShift Merge Robot 2021-11-06 10:39:16 +01:00 committed by GitHub
commit abbd6c167e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2190 additions and 657 deletions

View File

@ -8,7 +8,6 @@ import (
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -73,7 +72,8 @@ func port(_ *cobra.Command, args []string) error {
var (
container string
err error
userPort types.OCICNIPortMapping
userPort uint16
userProto string
)
if len(args) == 0 && !portOpts.Latest && !portOpts.All {
@ -101,16 +101,12 @@ func port(_ *cobra.Command, args []string) error {
fields = append(fields, "tcp")
}
portNum, err := strconv.Atoi(fields[0])
portNum, err := strconv.ParseUint(fields[0], 10, 16)
if err != nil {
return err
}
userPort = types.OCICNIPortMapping{
HostPort: 0,
ContainerPort: int32(portNum),
Protocol: fields[1],
HostIP: "",
}
userPort = uint16(portNum)
userProto = fields[1]
}
reports, err := registry.ContainerEngine().ContainerPort(registry.GetContext(), container, portOpts)
@ -120,24 +116,36 @@ func port(_ *cobra.Command, args []string) error {
var found bool
// Iterate mappings
for _, report := range reports {
allPrefix := ""
if portOpts.All {
allPrefix = report.Id[:12] + "\t"
}
for _, v := range report.Ports {
hostIP := v.HostIP
// Set host IP to 0.0.0.0 if blank
if hostIP == "" {
hostIP = "0.0.0.0"
}
if portOpts.All {
fmt.Printf("%s\t", report.Id[:12])
}
// If not searching by port or port/proto, then dump what we see
if port == "" {
fmt.Printf("%d/%s -> %s:%d\n", v.ContainerPort, v.Protocol, hostIP, v.HostPort)
continue
}
if v.ContainerPort == userPort.ContainerPort {
fmt.Printf("%s:%d\n", hostIP, v.HostPort)
found = true
break
protocols := strings.Split(v.Protocol, ",")
for _, protocol := range protocols {
// If not searching by port or port/proto, then dump what we see
if port == "" {
for i := uint16(0); i < v.Range; i++ {
fmt.Printf("%s%d/%s -> %s:%d\n", allPrefix, v.ContainerPort+i, protocol, hostIP, v.HostPort+i)
}
continue
}
// check if the proto matches and if the port is in the range
// this is faster than looping over the range for no reason
if v.Protocol == userProto &&
v.ContainerPort <= userPort &&
v.ContainerPort+v.Range > userPort {
// we have to add the current range to the host port
hostPort := v.HostPort + userPort - v.ContainerPort
fmt.Printf("%s%s:%d\n", allPrefix, hostIP, hostPort)
found = true
break
}
}
}
if !found && port != "" {

View File

@ -3,8 +3,6 @@ package containers
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
@ -477,174 +475,31 @@ func (l psReporter) UTS() string {
// portsToString converts the ports used to a string of the from "port1, port2"
// and also groups a continuous list of ports into a readable format.
func portsToString(ports []types.OCICNIPortMapping) string {
// The format is IP:HostPort(-Range)->ContainerPort(-Range)/Proto
func portsToString(ports []types.PortMapping) string {
if len(ports) == 0 {
return ""
}
// Sort the ports, so grouping continuous ports become easy.
sort.Slice(ports, func(i, j int) bool {
return comparePorts(ports[i], ports[j])
})
portGroups := [][]types.OCICNIPortMapping{}
currentGroup := []types.OCICNIPortMapping{}
for i, v := range ports {
var prevPort, nextPort *int32
if i > 0 {
prevPort = &ports[i-1].ContainerPort
}
if i+1 < len(ports) {
nextPort = &ports[i+1].ContainerPort
}
port := v.ContainerPort
// Helper functions
addToCurrentGroup := func(x types.OCICNIPortMapping) {
currentGroup = append(currentGroup, x)
}
addToPortGroup := func(x types.OCICNIPortMapping) {
portGroups = append(portGroups, []types.OCICNIPortMapping{x})
}
finishCurrentGroup := func() {
portGroups = append(portGroups, currentGroup)
currentGroup = []types.OCICNIPortMapping{}
}
// Single entry slice
if prevPort == nil && nextPort == nil {
addToPortGroup(v)
}
// Start of the slice with len > 0
if prevPort == nil && nextPort != nil {
isGroup := *nextPort-1 == port
if isGroup {
// Start with a group
addToCurrentGroup(v)
} else {
// Start with single item
addToPortGroup(v)
}
continue
}
// Middle of the slice with len > 0
if prevPort != nil && nextPort != nil {
currentIsGroup := *prevPort+1 == port
nextIsGroup := *nextPort-1 == port
if currentIsGroup {
// Maybe in the middle of a group
addToCurrentGroup(v)
if !nextIsGroup {
// End of a group
finishCurrentGroup()
}
} else if nextIsGroup {
// Start of a new group
addToCurrentGroup(v)
} else {
// No group at all
addToPortGroup(v)
}
continue
}
// End of the slice with len > 0
if prevPort != nil && nextPort == nil {
isGroup := *prevPort+1 == port
if isGroup {
// End group
addToCurrentGroup(v)
finishCurrentGroup()
} else {
// End single item
addToPortGroup(v)
}
}
}
portDisplay := []string{}
for _, group := range portGroups {
if len(group) == 0 {
// Usually should not happen, but better do not crash.
continue
}
first := group[0]
hostIP := first.HostIP
sb := &strings.Builder{}
for _, port := range ports {
hostIP := port.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
// Single mappings
if len(group) == 1 {
portDisplay = append(portDisplay,
fmt.Sprintf(
"%s:%d->%d/%s",
hostIP, first.HostPort, first.ContainerPort, first.Protocol,
),
)
continue
protocols := strings.Split(port.Protocol, ",")
for _, protocol := range protocols {
if port.Range > 1 {
fmt.Fprintf(sb, "%s:%d-%d->%d-%d/%s, ",
hostIP, port.HostPort, port.HostPort+port.Range-1,
port.ContainerPort, port.ContainerPort+port.Range-1, protocol)
} else {
fmt.Fprintf(sb, "%s:%d->%d/%s, ",
hostIP, port.HostPort,
port.ContainerPort, protocol)
}
}
// Group mappings
last := group[len(group)-1]
portDisplay = append(portDisplay, formatGroup(
fmt.Sprintf("%s/%s", hostIP, first.Protocol),
first.HostPort, last.HostPort,
first.ContainerPort, last.ContainerPort,
))
}
return strings.Join(portDisplay, ", ")
}
func comparePorts(i, j types.OCICNIPortMapping) bool {
if i.ContainerPort != j.ContainerPort {
return i.ContainerPort < j.ContainerPort
}
if i.HostIP != j.HostIP {
return i.HostIP < j.HostIP
}
if i.HostPort != j.HostPort {
return i.HostPort < j.HostPort
}
return i.Protocol < j.Protocol
}
// formatGroup returns the group in the format:
// <IP:firstHost:lastHost->firstCtr:lastCtr/Proto>
// e.g 0.0.0.0:1000-1006->2000-2006/tcp.
func formatGroup(key string, firstHost, lastHost, firstCtr, lastCtr int32) string {
parts := strings.Split(key, "/")
groupType := parts[0]
var ip string
if len(parts) > 1 {
ip = parts[0]
groupType = parts[1]
}
group := func(first, last int32) string {
group := strconv.Itoa(int(first))
if first != last {
group = fmt.Sprintf("%s-%d", group, last)
}
return group
}
hostGroup := group(firstHost, lastHost)
ctrGroup := group(firstCtr, lastCtr)
return fmt.Sprintf("%s:%s->%s/%s", ip, hostGroup, ctrGroup, groupType)
display := sb.String()
// make sure to trim the last ", " of the string
return display[:len(display)-2]
}

View File

@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/podman/v3/libpod/network/types"
@ -289,25 +290,30 @@ func handler(ctx context.Context, conn io.Reader, pm rkport.Manager) error {
return nil
}
func exposePorts(pm rkport.Manager, portMappings []types.OCICNIPortMapping, childIP string) error {
func exposePorts(pm rkport.Manager, portMappings []types.PortMapping, childIP string) error {
ctx := context.TODO()
for _, i := range portMappings {
hostIP := i.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
spec := rkport.Spec{
Proto: i.Protocol,
ParentIP: hostIP,
ParentPort: int(i.HostPort),
ChildPort: int(i.ContainerPort),
ChildIP: childIP,
}
if err := rkportutil.ValidatePortSpec(spec, nil); err != nil {
return err
}
if _, err := pm.AddPort(ctx, spec); err != nil {
return err
for _, port := range portMappings {
protocols := strings.Split(port.Protocol, ",")
for _, protocol := range protocols {
hostIP := port.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
for i := uint16(0); i < port.Range; i++ {
spec := rkport.Spec{
Proto: protocol,
ParentIP: hostIP,
ParentPort: int(port.HostPort + i),
ChildPort: int(port.ContainerPort + i),
ChildIP: childIP,
}
if err := rkportutil.ValidatePortSpec(spec, nil); err != nil {
return err
}
if _, err := pm.AddPort(ctx, spec); err != nil {
return err
}
}
}
}
return nil

View File

@ -384,6 +384,15 @@ func (s *BoltState) getContainerConfigFromDB(id []byte, config *ContainerConfig,
return errors.Wrapf(err, "error unmarshalling container %s config", string(id))
}
// convert ports to the new format if needed
if len(config.ContainerNetworkConfig.OldPortMappings) > 0 && len(config.ContainerNetworkConfig.PortMappings) == 0 {
config.ContainerNetworkConfig.PortMappings = ocicniPortsToNetTypesPorts(config.ContainerNetworkConfig.OldPortMappings)
// keep the OldPortMappings in case an user has to downgrade podman
// indicate the the config was modified and should be written back to the db when possible
config.rewrite = true
}
return nil
}

View File

@ -41,18 +41,20 @@ func getTestContainer(id, name string, manager lock.Manager) (*Container, error)
ContainerNetworkConfig: ContainerNetworkConfig{
DNSServer: []net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.2.2")},
DNSSearch: []string{"example.com", "example.example.com"},
PortMappings: []types.OCICNIPortMapping{
PortMappings: []types.PortMapping{
{
HostPort: 80,
ContainerPort: 90,
Protocol: "tcp",
HostIP: "192.168.3.3",
Range: 1,
},
{
HostPort: 100,
ContainerPort: 110,
Protocol: "udp",
HostIP: "192.168.4.4",
Range: 1,
},
},
},

View File

@ -465,7 +465,7 @@ func (c *Container) NewNetNS() bool {
// PortMappings returns the ports that will be mapped into a container if
// a new network namespace is created
// If NewNetNS() is false, this value is unused
func (c *Container) PortMappings() ([]types.OCICNIPortMapping, error) {
func (c *Container) PortMappings() ([]types.PortMapping, error) {
// First check if the container belongs to a network namespace (like a pod)
if len(c.config.NetNsCtr) > 0 {
netNsCtr, err := c.runtime.GetContainer(c.config.NetNsCtr)

View File

@ -78,6 +78,11 @@ type ContainerConfig struct {
// These containers must be started before this container is started.
Dependencies []string
// rewrite is an internal bool to indicate that the config was modified after
// a read from the db, e.g. to migrate config fields after an upgrade.
// This field should never be written to the db, the json tag ensures this.
rewrite bool `json:"-"`
// embedded sub-configs
ContainerRootFSConfig
ContainerSecurityConfig
@ -232,7 +237,12 @@ type ContainerNetworkConfig struct {
// PortMappings are the ports forwarded to the container's network
// namespace
// These are not used unless CreateNetNS is true
PortMappings []types.OCICNIPortMapping `json:"portMappings,omitempty"`
PortMappings []types.PortMapping `json:"newPortMappings,omitempty"`
// OldPortMappings are the ports forwarded to the container's network
// namespace. As of podman 4.0 this field is deprecated, use PortMappings
// instead. The db will convert the old ports to the new structure for you.
// These are not used unless CreateNetNS is true
OldPortMappings []types.OCICNIPortMapping `json:"portMappings,omitempty"`
// ExposedPorts are the ports which are exposed but not forwarded
// into the container.
// The map key is the port and the string slice contains the protocols,

View File

@ -667,6 +667,19 @@ func (c *Container) refresh() error {
c.state.NetworkStatus = nil
c.state.NetworkStatusOld = nil
// Rewrite the config if necessary.
// Podman 4.0 uses a new port format in the config.
// getContainerConfigFromDB() already converted the old ports to the new one
// but it did not write the config to the db back for performance reasons.
// If a rewrite must happen the config.rewrite field is set to true.
if c.config.rewrite {
// SafeRewriteContainerConfig must be used with care. Make sure to not change config fields by accident.
if err := c.runtime.state.SafeRewriteContainerConfig(c, "", "", c.config); err != nil {
return errors.Wrapf(err, "failed to rewrite the config for container %s", c.config.ID)
}
c.config.rewrite = false
}
if err := c.save(); err != nil {
return errors.Wrapf(err, "error refreshing state for container %s", c.ID())
}

View File

@ -75,7 +75,7 @@ func (p *Pod) GenerateForKube(ctx context.Context) (*v1.Pod, []v1.ServicePort, e
Hostnames: []string{hostSli[0]},
})
}
ports, err = ocicniPortMappingToContainerPort(infraContainer.config.PortMappings)
ports, err = portMappingToContainerPort(infraContainer.config.PortMappings)
if err != nil {
return nil, servicePorts, err
}
@ -452,7 +452,7 @@ func containerToV1Container(ctx context.Context, c *Container) (v1.Container, []
if err != nil {
return kubeContainer, kubeVolumes, nil, annotations, err
}
ports, err := ocicniPortMappingToContainerPort(portmappings)
ports, err := portMappingToContainerPort(portmappings)
if err != nil {
return kubeContainer, kubeVolumes, nil, annotations, err
}
@ -588,29 +588,36 @@ func containerToV1Container(ctx context.Context, c *Container) (v1.Container, []
return kubeContainer, kubeVolumes, &dns, annotations, nil
}
// ocicniPortMappingToContainerPort takes an ocicni portmapping and converts
// portMappingToContainerPort takes an portmapping and converts
// it to a v1.ContainerPort format for kube output
func ocicniPortMappingToContainerPort(portMappings []types.OCICNIPortMapping) ([]v1.ContainerPort, error) {
func portMappingToContainerPort(portMappings []types.PortMapping) ([]v1.ContainerPort, error) {
containerPorts := make([]v1.ContainerPort, 0, len(portMappings))
for _, p := range portMappings {
var protocol v1.Protocol
switch strings.ToUpper(p.Protocol) {
case "TCP":
// do nothing as it is the default protocol in k8s, there is no need to explicitly
// add it to the generated yaml
case "UDP":
protocol = v1.ProtocolUDP
default:
return containerPorts, errors.Errorf("unknown network protocol %s", p.Protocol)
protocols := strings.Split(p.Protocol, ",")
for _, proto := range protocols {
var protocol v1.Protocol
switch strings.ToUpper(proto) {
case "TCP":
// do nothing as it is the default protocol in k8s, there is no need to explicitly
// add it to the generated yaml
case "UDP":
protocol = v1.ProtocolUDP
case "SCTP":
protocol = v1.ProtocolSCTP
default:
return containerPorts, errors.Errorf("unknown network protocol %s", p.Protocol)
}
for i := uint16(0); i < p.Range; i++ {
cp := v1.ContainerPort{
// Name will not be supported
HostPort: int32(p.HostPort + i),
HostIP: p.HostIP,
ContainerPort: int32(p.ContainerPort + i),
Protocol: protocol,
}
containerPorts = append(containerPorts, cp)
}
}
cp := v1.ContainerPort{
// Name will not be supported
HostPort: p.HostPort,
HostIP: p.HostIP,
ContainerPort: p.ContainerPort,
Protocol: protocol,
}
containerPorts = append(containerPorts, cp)
}
return containerPorts, nil
}

View File

@ -11,6 +11,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"syscall"
@ -91,10 +92,7 @@ func (c *Container) getNetworkOptions() (types.NetworkOptions, error) {
ContainerID: c.config.ID,
ContainerName: getCNIPodName(c),
}
// TODO remove ocicni PortMappings from container config and store as types PortMappings
if len(c.config.PortMappings) > 0 {
opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings)
}
opts.PortMappings = c.config.PortMappings
networks, _, err := c.networks()
if err != nil {
return opts, err
@ -1209,9 +1207,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro
ContainerID: c.config.ID,
ContainerName: getCNIPodName(c),
}
if len(c.config.PortMappings) > 0 {
opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings)
}
opts.PortMappings = c.config.PortMappings
eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(netName)
if !exists {
return errors.Errorf("no network interface name for container %s on network %s", c.config.ID, netName)
@ -1303,9 +1299,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e
ContainerID: c.config.ID,
ContainerName: getCNIPodName(c),
}
if len(c.config.PortMappings) > 0 {
opts.PortMappings = ocicniPortsToNetTypesPorts(c.config.PortMappings)
}
opts.PortMappings = c.config.PortMappings
eth, exists := c.state.NetInterfaceDescriptions.getInterfaceByName(netName)
if !exists {
return errors.Errorf("no network interface name for container %s on network %s", c.config.ID, netName)
@ -1373,16 +1367,67 @@ func (r *Runtime) normalizeNetworkName(nameOrID string) (string, error) {
return net.Name, nil
}
// ocicniPortsToNetTypesPorts convert the old port format to the new one
// while deduplicating ports into ranges
func ocicniPortsToNetTypesPorts(ports []types.OCICNIPortMapping) []types.PortMapping {
newPorts := make([]types.PortMapping, 0, len(ports))
for _, port := range ports {
newPorts = append(newPorts, types.PortMapping{
HostIP: port.HostIP,
HostPort: uint16(port.HostPort),
ContainerPort: uint16(port.ContainerPort),
Protocol: port.Protocol,
Range: 1,
})
if len(ports) == 0 {
return nil
}
newPorts := make([]types.PortMapping, 0, len(ports))
// first sort the ports
sort.Slice(ports, func(i, j int) bool {
return compareOCICNIPorts(ports[i], ports[j])
})
// we already check if the slice is empty so we can use the first element
currentPort := types.PortMapping{
HostIP: ports[0].HostIP,
HostPort: uint16(ports[0].HostPort),
ContainerPort: uint16(ports[0].ContainerPort),
Protocol: ports[0].Protocol,
Range: 1,
}
for i := 1; i < len(ports); i++ {
if ports[i].HostIP == currentPort.HostIP &&
ports[i].Protocol == currentPort.Protocol &&
ports[i].HostPort-int32(currentPort.Range) == int32(currentPort.HostPort) &&
ports[i].ContainerPort-int32(currentPort.Range) == int32(currentPort.ContainerPort) {
currentPort.Range = currentPort.Range + 1
} else {
newPorts = append(newPorts, currentPort)
currentPort = types.PortMapping{
HostIP: ports[i].HostIP,
HostPort: uint16(ports[i].HostPort),
ContainerPort: uint16(ports[i].ContainerPort),
Protocol: ports[i].Protocol,
Range: 1,
}
}
}
newPorts = append(newPorts, currentPort)
return newPorts
}
// compareOCICNIPorts will sort the ocicni ports by
// 1) host ip
// 2) protocol
// 3) hostPort
// 4) container port
func compareOCICNIPorts(i, j types.OCICNIPortMapping) bool {
if i.HostIP != j.HostIP {
return i.HostIP < j.HostIP
}
if i.Protocol != j.Protocol {
return i.Protocol < j.Protocol
}
if i.HostPort != j.HostPort {
return i.HostPort < j.HostPort
}
return i.ContainerPort < j.ContainerPort
}

View File

@ -0,0 +1,323 @@
package libpod
import (
"fmt"
"testing"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/stretchr/testify/assert"
)
func Test_ocicniPortsToNetTypesPorts(t *testing.T) {
tests := []struct {
name string
arg []types.OCICNIPortMapping
want []types.PortMapping
}{
{
name: "no ports",
arg: nil,
want: nil,
},
{
name: "empty ports",
arg: []types.OCICNIPortMapping{},
want: nil,
},
{
name: "single port",
arg: []types.OCICNIPortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "two separate ports",
arg: []types.OCICNIPortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 9000,
ContainerPort: 90,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 9000,
ContainerPort: 90,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "two ports joined",
arg: []types.OCICNIPortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 2,
},
},
},
{
name: "three ports with different container port are not joined",
arg: []types.OCICNIPortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 8081,
ContainerPort: 79,
Protocol: "tcp",
},
{
HostPort: 8082,
ContainerPort: 82,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 8081,
ContainerPort: 79,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 8082,
ContainerPort: 82,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "three ports joined (not sorted)",
arg: []types.OCICNIPortMapping{
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "tcp",
},
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 8082,
ContainerPort: 82,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 3,
},
},
},
{
name: "different protocols ports are not joined",
arg: []types.OCICNIPortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "udp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "udp",
Range: 1,
},
},
},
{
name: "different host ip ports are not joined",
arg: []types.OCICNIPortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
HostIP: "192.168.1.1",
},
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "tcp",
HostIP: "192.168.1.2",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
HostIP: "192.168.1.1",
},
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "tcp",
Range: 1,
HostIP: "192.168.1.2",
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
result := ocicniPortsToNetTypesPorts(tt.arg)
assert.Equal(t, tt.want, result, "ports do not match")
})
}
}
func benchmarkOCICNIPortsToNetTypesPorts(b *testing.B, ports []types.OCICNIPortMapping) {
for n := 0; n < b.N; n++ {
ocicniPortsToNetTypesPorts(ports)
}
}
func Benchmark_ocicniPortsToNetTypesPortsNoPorts(b *testing.B) {
benchmarkOCICNIPortsToNetTypesPorts(b, nil)
}
func Benchmark_ocicniPortsToNetTypesPorts1(b *testing.B) {
benchmarkOCICNIPortsToNetTypesPorts(b, []types.OCICNIPortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
})
}
func Benchmark_ocicniPortsToNetTypesPorts10(b *testing.B) {
ports := make([]types.OCICNIPortMapping, 0, 10)
for i := int32(8080); i < 8090; i++ {
ports = append(ports, types.OCICNIPortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkOCICNIPortsToNetTypesPorts(b, ports)
}
func Benchmark_ocicniPortsToNetTypesPorts100(b *testing.B) {
ports := make([]types.OCICNIPortMapping, 0, 100)
for i := int32(8080); i < 8180; i++ {
ports = append(ports, types.OCICNIPortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkOCICNIPortsToNetTypesPorts(b, ports)
}
func Benchmark_ocicniPortsToNetTypesPorts1k(b *testing.B) {
ports := make([]types.OCICNIPortMapping, 0, 1000)
for i := int32(8080); i < 9080; i++ {
ports = append(ports, types.OCICNIPortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkOCICNIPortsToNetTypesPorts(b, ports)
}
func Benchmark_ocicniPortsToNetTypesPorts10k(b *testing.B) {
ports := make([]types.OCICNIPortMapping, 0, 30000)
for i := int32(8080); i < 18080; i++ {
ports = append(ports, types.OCICNIPortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkOCICNIPortsToNetTypesPorts(b, ports)
}
func Benchmark_ocicniPortsToNetTypesPorts1m(b *testing.B) {
ports := make([]types.OCICNIPortMapping, 0, 1000000)
for j := 0; j < 20; j++ {
for i := int32(1); i <= 50000; i++ {
ports = append(ports, types.OCICNIPortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
HostIP: fmt.Sprintf("192.168.1.%d", j),
})
}
}
b.ResetTimer()
benchmarkOCICNIPortsToNetTypesPorts(b, ports)
}

View File

@ -38,9 +38,9 @@ type slirpFeatures struct {
type slirp4netnsCmdArg struct {
Proto string `json:"proto,omitempty"`
HostAddr string `json:"host_addr"`
HostPort int32 `json:"host_port"`
HostPort uint16 `json:"host_port"`
GuestAddr string `json:"guest_addr"`
GuestPort int32 `json:"guest_port"`
GuestPort uint16 `json:"guest_port"`
}
type slirp4netnsCmd struct {

View File

@ -32,95 +32,110 @@ func createUnitName(prefix string, name string) string {
}
// Bind ports to keep them closed on the host
func bindPorts(ports []types.OCICNIPortMapping) ([]*os.File, error) {
func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
var files []*os.File
notifySCTP := false
for _, i := range ports {
isV6 := net.ParseIP(i.HostIP).To4() == nil
if i.HostIP == "" {
sctpWarning := true
for _, port := range ports {
isV6 := net.ParseIP(port.HostIP).To4() == nil
if port.HostIP == "" {
isV6 = false
}
switch i.Protocol {
case "udp":
var (
addr *net.UDPAddr
err error
)
if isV6 {
addr, err = net.ResolveUDPAddr("udp6", fmt.Sprintf("[%s]:%d", i.HostIP, i.HostPort))
} else {
addr, err = net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", i.HostIP, i.HostPort))
protocols := strings.Split(port.Protocol, ",")
for _, protocol := range protocols {
for i := uint16(0); i < port.Range; i++ {
f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning)
if err != nil {
return files, err
}
if f != nil {
files = append(files, f)
}
}
if err != nil {
return nil, errors.Wrapf(err, "cannot resolve the UDP address")
}
proto := "udp4"
if isV6 {
proto = "udp6"
}
server, err := net.ListenUDP(proto, addr)
if err != nil {
return nil, errors.Wrapf(err, "cannot listen on the UDP port")
}
f, err := server.File()
if err != nil {
return nil, errors.Wrapf(err, "cannot get file for UDP socket")
}
files = append(files, f)
// close the listener
// note that this does not affect the fd, see the godoc for server.File()
err = server.Close()
if err != nil {
logrus.Warnf("Failed to close connection: %v", err)
}
case "tcp":
var (
addr *net.TCPAddr
err error
)
if isV6 {
addr, err = net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", i.HostIP, i.HostPort))
} else {
addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", i.HostIP, i.HostPort))
}
if err != nil {
return nil, errors.Wrapf(err, "cannot resolve the TCP address")
}
proto := "tcp4"
if isV6 {
proto = "tcp6"
}
server, err := net.ListenTCP(proto, addr)
if err != nil {
return nil, errors.Wrapf(err, "cannot listen on the TCP port")
}
f, err := server.File()
if err != nil {
return nil, errors.Wrapf(err, "cannot get file for TCP socket")
}
files = append(files, f)
// close the listener
// note that this does not affect the fd, see the godoc for server.File()
err = server.Close()
if err != nil {
logrus.Warnf("Failed to close connection: %v", err)
}
case "sctp":
if !notifySCTP {
notifySCTP = true
logrus.Warnf("Port reservation for SCTP is not supported")
}
default:
return nil, fmt.Errorf("unknown protocol %s", i.Protocol)
}
}
return files, nil
}
func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool) (*os.File, error) {
var file *os.File
switch protocol {
case "udp":
var (
addr *net.UDPAddr
err error
)
if isV6 {
addr, err = net.ResolveUDPAddr("udp6", fmt.Sprintf("[%s]:%d", hostIP, port))
} else {
addr, err = net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", hostIP, port))
}
if err != nil {
return nil, errors.Wrapf(err, "cannot resolve the UDP address")
}
proto := "udp4"
if isV6 {
proto = "udp6"
}
server, err := net.ListenUDP(proto, addr)
if err != nil {
return nil, errors.Wrapf(err, "cannot listen on the UDP port")
}
file, err = server.File()
if err != nil {
return nil, errors.Wrapf(err, "cannot get file for UDP socket")
}
// close the listener
// note that this does not affect the fd, see the godoc for server.File()
err = server.Close()
if err != nil {
logrus.Warnf("Failed to close connection: %v", err)
}
case "tcp":
var (
addr *net.TCPAddr
err error
)
if isV6 {
addr, err = net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", hostIP, port))
} else {
addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", hostIP, port))
}
if err != nil {
return nil, errors.Wrapf(err, "cannot resolve the TCP address")
}
proto := "tcp4"
if isV6 {
proto = "tcp6"
}
server, err := net.ListenTCP(proto, addr)
if err != nil {
return nil, errors.Wrapf(err, "cannot listen on the TCP port")
}
file, err = server.File()
if err != nil {
return nil, errors.Wrapf(err, "cannot get file for TCP socket")
}
// close the listener
// note that this does not affect the fd, see the godoc for server.File()
err = server.Close()
if err != nil {
logrus.Warnf("Failed to close connection: %v", err)
}
case "sctp":
if *sctpWarning {
logrus.Info("Port reservation for SCTP is not supported")
*sctpWarning = false
}
default:
return nil, fmt.Errorf("unknown protocol %s", protocol)
}
return file, nil
}
func getOCIRuntimeError(runtimeMsg string) error {
includeFullOutput := logrus.GetLevel() == logrus.DebugLevel

View File

@ -1051,7 +1051,7 @@ func WithDependencyCtrs(ctrs []*Container) CtrCreateOption {
// namespace with a minimal configuration.
// An optional array of port mappings can be provided.
// Conflicts with WithNetNSFrom().
func WithNetNS(portMappings []nettypes.OCICNIPortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption {
func WithNetNS(portMappings []nettypes.PortMapping, exposedPorts map[uint16][]string, postConfigureNetNS bool, netmode string, networks []string) CtrCreateOption {
return func(ctr *Container) error {
if ctr.valid {
return define.ErrCtrFinalized
@ -2084,21 +2084,6 @@ func WithInfraContainer() PodCreateOption {
}
}
// WithInfraContainerPorts tells the pod to add port bindings to the pause container
func WithInfraContainerPorts(bindings []nettypes.OCICNIPortMapping, infraSpec *specgen.SpecGenerator) []nettypes.PortMapping {
bindingSpec := []nettypes.PortMapping{}
for _, bind := range bindings {
currBind := nettypes.PortMapping{}
currBind.ContainerPort = uint16(bind.ContainerPort)
currBind.HostIP = bind.HostIP
currBind.HostPort = uint16(bind.HostPort)
currBind.Protocol = bind.Protocol
bindingSpec = append(bindingSpec, currBind)
}
infraSpec.PortMappings = bindingSpec
return infraSpec.PortMappings
}
// WithVolatile sets the volatile flag for the container storage.
// The option can potentially cause data loss when used on a container that must survive a machine reboot.
func WithVolatile() CtrCreateOption {

View File

@ -11,6 +11,7 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/libpod/lock"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/storage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -3705,3 +3706,42 @@ func TestGetContainerConfigNonExistentIDFails(t *testing.T) {
assert.Error(t, err)
})
}
// Test that the state will convert the ports to the new format
func TestConvertPortMapping(t *testing.T) {
runForAllStates(t, func(t *testing.T, state State, manager lock.Manager) {
testCtr, err := getTestCtr1(manager)
assert.NoError(t, err)
ports := testCtr.config.PortMappings
oldPorts := []types.OCICNIPortMapping{
{
HostPort: 80,
ContainerPort: 90,
Protocol: "tcp",
HostIP: "192.168.3.3",
},
{
HostPort: 100,
ContainerPort: 110,
Protocol: "udp",
HostIP: "192.168.4.4",
},
}
testCtr.config.OldPortMappings = oldPorts
testCtr.config.PortMappings = nil
err = state.AddContainer(testCtr)
assert.NoError(t, err)
retrievedCtr, err := state.Container(testCtr.ID())
assert.NoError(t, err)
// set values to expected ones
testCtr.config.PortMappings = ports
testContainersEqual(t, retrievedCtr, testCtr, true)
})
}

View File

@ -295,19 +295,21 @@ func writeHijackHeader(r *http.Request, conn io.Writer) {
}
// Convert OCICNI port bindings into Inspect-formatted port bindings.
func makeInspectPortBindings(bindings []types.OCICNIPortMapping, expose map[uint16][]string) map[string][]define.InspectHostPort {
func makeInspectPortBindings(bindings []types.PortMapping, expose map[uint16][]string) map[string][]define.InspectHostPort {
portBindings := make(map[string][]define.InspectHostPort)
for _, port := range bindings {
key := fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol)
hostPorts := portBindings[key]
if hostPorts == nil {
hostPorts = []define.InspectHostPort{}
protocols := strings.Split(port.Protocol, ",")
for _, protocol := range protocols {
for i := uint16(0); i < port.Range; i++ {
key := fmt.Sprintf("%d/%s", port.ContainerPort+i, protocol)
hostPorts := portBindings[key]
hostPorts = append(hostPorts, define.InspectHostPort{
HostIP: port.HostIP,
HostPort: fmt.Sprintf("%d", port.HostPort+i),
})
portBindings[key] = hostPorts
}
}
hostPorts = append(hostPorts, define.InspectHostPort{
HostIP: port.HostIP,
HostPort: fmt.Sprintf("%d", port.HostPort),
})
portBindings[key] = hostPorts
}
// add exposed ports without host port information to match docker
for port, protocols := range expose {

View File

@ -193,7 +193,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt
}
if len(restoreOptions.PublishPorts) > 0 {
ports, _, _, err := generate.ParsePortMapping(restoreOptions.PublishPorts)
ports, err := generate.ParsePortMapping(restoreOptions.PublishPorts, nil)
if err != nil {
return nil, err
}

View File

@ -54,7 +54,7 @@ type ListContainer struct {
// boolean to be set
PodName string
// Port mappings
Ports []types.OCICNIPortMapping
Ports []types.PortMapping
// Size of the container rootfs. Requires the size boolean to be true
Size *define.ContainerSize
// Time when container started

View File

@ -422,7 +422,7 @@ type ContainerPortOptions struct {
// the CLI to output ports
type ContainerPortReport struct {
Id string //nolint
Ports []nettypes.OCICNIPortMapping
Ports []nettypes.PortMapping
}
// ContainerCpOptions describes input options for cp.

View File

@ -23,7 +23,7 @@ const (
// Config needs to be provided to the process via stdin as a JSON string.
// stdin needs to be closed after the message has been written.
type Config struct {
Mappings []types.OCICNIPortMapping
Mappings []types.PortMapping
NetNSPath string
ExitFD int
ReadyFD int

View File

@ -204,11 +204,11 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime, infraSpec
// replacing necessary values with those specified in pod creation
func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) {
if len(p.PortMappings) > 0 {
ports, _, _, err := ParsePortMapping(p.PortMappings)
ports, err := ParsePortMapping(p.PortMappings, nil)
if err != nil {
return nil, err
}
p.InfraContainerSpec.PortMappings = libpod.WithInfraContainerPorts(ports, p.InfraContainerSpec)
p.InfraContainerSpec.PortMappings = ports
}
switch p.NetNS.NSMode {
case specgen.Default, "":

View File

@ -2,7 +2,9 @@ package generate
import (
"context"
"fmt"
"net"
"sort"
"strconv"
"strings"
@ -11,6 +13,7 @@ import (
"github.com/containers/podman/v3/utils"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/podman/v3/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -21,252 +24,323 @@ const (
protoSCTP = "sctp"
)
// Parse port maps to OCICNI port mappings.
// Returns a set of OCICNI port mappings, and maps of utilized container and
// joinTwoPortsToRangePortIfPossible will expect two ports the previous port one must have a lower or equal hostPort than the current port.
func joinTwoPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool,
previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) {
// no previous port just return the current one
if previousPort == nil {
return &port, nil
}
if previousPort.HostPort+previousPort.Range >= port.HostPort {
// check if the port range matches the host and container ports
portDiff := port.HostPort - previousPort.HostPort
if portDiff == port.ContainerPort-previousPort.ContainerPort {
// calc the new range use the old range and add the difference between the ports
newRange := port.Range + portDiff
// if the newRange is greater than the old range use it
// this is important otherwise we would could lower the range
if newRange > previousPort.Range {
previousPort.Range = newRange
}
return previousPort, nil
}
// if both host port ranges overlap and the container port range did not match
// we have to error because we cannot assign the same host port to more than one container port
if previousPort.HostPort+previousPort.Range-1 > port.HostPort {
return nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", port.HostPort, port.Protocol)
}
}
// we could not join the ports so we append the old one to the list
// and return the current port as previous port
addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, previousPort)
return &port, nil
}
// joinTwoContainerPortsToRangePortIfPossible will expect two ports with both no host port set,
// the previous port one must have a lower or equal containerPort than the current port.
func joinTwoContainerPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool,
previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) {
// no previous port just return the current one
if previousPort == nil {
return &port, nil
}
if previousPort.ContainerPort+previousPort.Range > port.ContainerPort {
// calc the new range use the old range and add the difference between the ports
newRange := port.ContainerPort - previousPort.ContainerPort + port.Range
// if the newRange is greater than the old range use it
// this is important otherwise we would could lower the range
if newRange > previousPort.Range {
previousPort.Range = newRange
}
return previousPort, nil
}
// we could not join the ports so we append the old one to the list
// and return the current port as previous port
newPort, err := getRandomHostPort(currentHostPorts, *previousPort)
if err != nil {
return nil, err
}
addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, &newPort)
return &port, nil
}
func addPortToUsedPorts(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, port *types.PortMapping) {
for i := uint16(0); i < port.Range; i++ {
h := port.HostPort + i
allHostPorts[h] = true
currentHostPorts[h] = true
c := port.ContainerPort + i
allContainerPorts[c] = true
}
*ports = append(*ports, *port)
}
// getRandomHostPort get a random host port mapping for the given port
// the caller has to supply a array with he already used ports
func getRandomHostPort(hostPorts *[65536]bool, port types.PortMapping) (types.PortMapping, error) {
outer:
for i := 0; i < 15; i++ {
ranPort, err := utils.GetRandomPort()
if err != nil {
return port, err
}
// if port range is exceeds max port we cannot use it
if ranPort+int(port.Range) > 65535 {
continue
}
// check if there is a port in the range which is used
for j := 0; j < int(port.Range); j++ {
// port already used
if hostPorts[ranPort+j] {
continue outer
}
}
port.HostPort = uint16(ranPort)
return port, nil
}
// add range to error message if needed
rangePort := ""
if port.Range > 1 {
rangePort = fmt.Sprintf("with range %d ", port.Range)
}
return port, errors.Errorf("failed to find an open port to expose container port %d %son the host", port.ContainerPort, rangePort)
}
// Parse port maps to port mappings.
// Returns a set of port mappings, and maps of utilized container and
// host ports.
func ParsePortMapping(portMappings []types.PortMapping) ([]types.OCICNIPortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) {
// First, we need to validate the ports passed in the specgen, and then
// convert them into CNI port mappings.
func ParsePortMapping(portMappings []types.PortMapping, exposePorts map[uint16][]string) ([]types.PortMapping, error) {
if len(portMappings) == 0 && len(exposePorts) == 0 {
return nil, nil
}
// tempMapping stores the ports without ip and protocol
type tempMapping struct {
mapping types.OCICNIPortMapping
startOfRange bool
isInRange bool
}
tempMappings := []tempMapping{}
// To validate, we need two maps: one for host ports, one for container
// ports.
// Each is a map of protocol to map of IP address to map of port to
// port (for hostPortValidate, it's host port to container port;
// for containerPortValidate, container port to host port.
// These will ensure no collisions.
hostPortValidate := make(map[string]map[string]map[uint16]uint16)
containerPortValidate := make(map[string]map[string]map[uint16]uint16)
// Initialize the first level of maps (we can't really guess keys for
// the rest).
for _, proto := range []string{protoTCP, protoUDP, protoSCTP} {
hostPortValidate[proto] = make(map[string]map[uint16]uint16)
containerPortValidate[proto] = make(map[string]map[uint16]uint16)
hostPort uint16
containerPort uint16
rangePort uint16
}
postAssignHostPort := false
// portMap is a temporary structure to sort all ports
// the map is hostIp -> protocol -> array of mappings
portMap := make(map[string]map[string][]tempMapping)
// Iterate through all port mappings, generating OCICNI PortMapping
// structs and validating there is no overlap.
// allUsedContainerPorts stores all used ports for each protocol
// the key is the protocol and the array is 65536 elements long for each port.
allUsedContainerPortsMap := make(map[string][65536]bool)
allUsedHostPortsMap := make(map[string][65536]bool)
// First, we need to validate the ports passed in the specgen
for _, port := range portMappings {
// First, check proto
protocols, err := checkProtocol(port.Protocol, true)
if err != nil {
return nil, nil, nil, err
return nil, err
}
// Validate host IP
hostIP := port.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
if ip := net.ParseIP(hostIP); ip == nil {
return nil, nil, nil, errors.Errorf("invalid IP address %s in port mapping", port.HostIP)
if port.HostIP != "" {
if ip := net.ParseIP(port.HostIP); ip == nil {
return nil, errors.Errorf("invalid IP address %q in port mapping", port.HostIP)
}
}
// Validate port numbers and range.
len := port.Range
if len == 0 {
len = 1
portRange := port.Range
if portRange == 0 {
portRange = 1
}
containerPort := port.ContainerPort
if containerPort == 0 {
return nil, nil, nil, errors.Errorf("container port number must be non-0")
return nil, errors.Errorf("container port number must be non-0")
}
hostPort := port.HostPort
if uint32(len-1)+uint32(containerPort) > 65535 {
return nil, nil, nil, errors.Errorf("container port range exceeds maximum allowable port number")
if uint32(portRange-1)+uint32(containerPort) > 65535 {
return nil, errors.Errorf("container port range exceeds maximum allowable port number")
}
if uint32(len-1)+uint32(hostPort) > 65536 {
return nil, nil, nil, errors.Errorf("host port range exceeds maximum allowable port number")
if uint32(portRange-1)+uint32(hostPort) > 65535 {
return nil, errors.Errorf("host port range exceeds maximum allowable port number")
}
// Iterate through ports, populating maps to check for conflicts
// and generating CNI port mappings.
for _, p := range protocols {
hostIPMap := hostPortValidate[p]
ctrIPMap := containerPortValidate[p]
hostPortMap, ok := hostIPMap[hostIP]
if !ok {
hostPortMap = make(map[uint16]uint16)
hostIPMap[hostIP] = hostPortMap
}
ctrPortMap, ok := ctrIPMap[hostIP]
if !ok {
ctrPortMap = make(map[uint16]uint16)
ctrIPMap[hostIP] = ctrPortMap
hostProtoMap, ok := portMap[port.HostIP]
if !ok {
hostProtoMap = make(map[string][]tempMapping)
for _, proto := range []string{protoTCP, protoUDP, protoSCTP} {
hostProtoMap[proto] = make([]tempMapping, 0)
}
portMap[port.HostIP] = hostProtoMap
}
// Iterate through all port numbers in the requested
// range.
var index uint16
for index = 0; index < len; index++ {
cPort := containerPort + index
hPort := hostPort
// Only increment host port if it's not 0.
if hostPort != 0 {
hPort += index
}
p := tempMapping{
hostPort: port.HostPort,
containerPort: port.ContainerPort,
rangePort: portRange,
}
if cPort == 0 {
return nil, nil, nil, errors.Errorf("container port cannot be 0")
}
// Host port is allowed to be 0. If it is, we
// select a random port on the host.
// This will happen *after* all other ports are
// placed, to ensure we don't accidentally
// select a port that a later mapping wanted.
if hPort == 0 {
// If we already have a host port
// assigned to their container port -
// just use that.
if ctrPortMap[cPort] != 0 {
hPort = ctrPortMap[cPort]
} else {
postAssignHostPort = true
}
} else {
testHPort := hostPortMap[hPort]
if testHPort != 0 && testHPort != cPort {
return nil, nil, nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", hPort, p)
}
hostPortMap[hPort] = cPort
// Mapping a container port to multiple
// host ports is allowed.
// We only store the latest of these in
// the container port map - we don't
// need to know all of them, just one.
testCPort := ctrPortMap[cPort]
ctrPortMap[cPort] = hPort
// If we have an exact duplicate, just continue
if testCPort == hPort && testHPort == cPort {
continue
}
}
// We appear to be clear. Make an OCICNI port
// struct.
// Don't use hostIP - we want to preserve the
// empty string hostIP by default for compat.
cniPort := types.OCICNIPortMapping{
HostPort: int32(hPort),
ContainerPort: int32(cPort),
Protocol: p,
HostIP: port.HostIP,
}
tempMappings = append(
tempMappings,
tempMapping{
mapping: cniPort,
startOfRange: port.Range > 1 && index == 0,
isInRange: port.Range > 1,
},
)
}
for _, proto := range protocols {
hostProtoMap[proto] = append(hostProtoMap[proto], p)
}
}
// Handle any 0 host ports now by setting random container ports.
if postAssignHostPort {
remadeMappings := make([]types.OCICNIPortMapping, 0, len(tempMappings))
// we do no longer need the original port mappings
// set it to 0 length so we can resuse it to populate
// the slice again while keeping the underlying capacity
portMappings = portMappings[:0]
var (
candidate int
err error
)
// Iterate over all
for _, tmp := range tempMappings {
p := tmp.mapping
if p.HostPort != 0 {
remadeMappings = append(remadeMappings, p)
for hostIP, protoMap := range portMap {
for protocol, ports := range protoMap {
ports := ports
if len(ports) == 0 {
continue
}
hostIPMap := hostPortValidate[p.Protocol]
ctrIPMap := containerPortValidate[p.Protocol]
hostPortMap, ok := hostIPMap[p.HostIP]
if !ok {
hostPortMap = make(map[uint16]uint16)
hostIPMap[p.HostIP] = hostPortMap
}
ctrPortMap, ok := ctrIPMap[p.HostIP]
if !ok {
ctrPortMap = make(map[uint16]uint16)
ctrIPMap[p.HostIP] = ctrPortMap
}
// See if container port has been used elsewhere
if ctrPortMap[uint16(p.ContainerPort)] != 0 {
// Duplicate definition. Let's not bother
// including it.
continue
}
// Max retries to ensure we don't loop forever.
for i := 0; i < 15; i++ {
// Only get a random candidate for single entries or the start
// of a range. Otherwise we just increment the candidate.
if !tmp.isInRange || tmp.startOfRange {
candidate, err = utils.GetRandomPort()
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort)
}
} else {
candidate++
// 1. sort the ports by host port
// use a small hack to make sure ports with host port 0 are sorted last
sort.Slice(ports, func(i, j int) bool {
if ports[i].hostPort == ports[j].hostPort {
return ports[i].containerPort < ports[j].containerPort
}
if ports[i].hostPort == 0 {
return false
}
if ports[j].hostPort == 0 {
return true
}
return ports[i].hostPort < ports[j].hostPort
})
if hostPortMap[uint16(candidate)] == 0 {
logrus.Debugf("Successfully assigned container port %d to host port %d (IP %s Protocol %s)", p.ContainerPort, candidate, p.HostIP, p.Protocol)
hostPortMap[uint16(candidate)] = uint16(p.ContainerPort)
ctrPortMap[uint16(p.ContainerPort)] = uint16(candidate)
p.HostPort = int32(candidate)
allUsedContainerPorts := allUsedContainerPortsMap[protocol]
allUsedHostPorts := allUsedHostPortsMap[protocol]
var usedHostPorts [65536]bool
var previousPort *types.PortMapping
var i int
for i = 0; i < len(ports); i++ {
if ports[i].hostPort == 0 {
// because the ports are sorted and host port 0 is last
// we can break when we hit 0
// we will fit them in afterwards
break
}
p := types.PortMapping{
HostIP: hostIP,
Protocol: protocol,
HostPort: ports[i].hostPort,
ContainerPort: ports[i].containerPort,
Range: ports[i].rangePort,
}
var err error
previousPort, err = joinTwoPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts,
&allUsedContainerPorts, &usedHostPorts, previousPort, p)
if err != nil {
return nil, err
}
}
if p.HostPort == 0 {
return nil, nil, nil, errors.Errorf("could not find open host port to map container port %d to", p.ContainerPort)
if previousPort != nil {
addPortToUsedPorts(&portMappings, &allUsedHostPorts,
&allUsedContainerPorts, &usedHostPorts, previousPort)
}
remadeMappings = append(remadeMappings, p)
// now take care of the hostPort = 0 ports
previousPort = nil
for i < len(ports) {
p := types.PortMapping{
HostIP: hostIP,
Protocol: protocol,
ContainerPort: ports[i].containerPort,
Range: ports[i].rangePort,
}
var err error
previousPort, err = joinTwoContainerPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts,
&allUsedContainerPorts, &usedHostPorts, previousPort, p)
if err != nil {
return nil, err
}
i++
}
if previousPort != nil {
newPort, err := getRandomHostPort(&usedHostPorts, *previousPort)
if err != nil {
return nil, err
}
addPortToUsedPorts(&portMappings, &allUsedHostPorts,
&allUsedContainerPorts, &usedHostPorts, &newPort)
}
allUsedContainerPortsMap[protocol] = allUsedContainerPorts
allUsedHostPortsMap[protocol] = allUsedHostPorts
}
return remadeMappings, containerPortValidate, hostPortValidate, nil
}
finalMappings := []types.OCICNIPortMapping{}
for _, m := range tempMappings {
finalMappings = append(finalMappings, m.mapping)
}
if len(exposePorts) > 0 {
logrus.Debugf("Adding exposed ports")
return finalMappings, containerPortValidate, hostPortValidate, nil
for port, protocols := range exposePorts {
newProtocols := make([]string, 0, len(protocols))
for _, protocol := range protocols {
if !allUsedContainerPortsMap[protocol][port] {
p := types.PortMapping{
ContainerPort: port,
Protocol: protocol,
Range: 1,
}
allPorts := allUsedContainerPortsMap[protocol]
p, err := getRandomHostPort(&allPorts, p)
if err != nil {
return nil, err
}
portMappings = append(portMappings, p)
} else {
newProtocols = append(newProtocols, protocol)
}
}
// make sure to delete the key from the map if there are no protocols left
if len(newProtocols) == 0 {
delete(exposePorts, port)
} else {
exposePorts[port] = newProtocols
}
}
}
return portMappings, nil
}
func appendProtocolsNoDuplicates(slice []string, protocols []string) []string {
for _, proto := range protocols {
if util.StringInSlice(proto, slice) {
continue
}
slice = append(slice, proto)
}
return slice
}
// Make final port mappings for the container
func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.OCICNIPortMapping, map[uint16][]string, error) {
finalMappings, containerPortValidate, hostPortValidate, err := ParsePortMapping(s.PortMappings)
if err != nil {
return nil, nil, err
}
// No exposed ports so return the port mappings we've made so far.
if len(s.Expose) == 0 && imageData == nil {
return finalMappings, nil, nil
}
logrus.Debugf("Adding exposed ports")
func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.PortMapping, map[uint16][]string, error) {
expose := make(map[uint16]string)
var err error
if imageData != nil {
expose, err = GenExposedPorts(imageData.Config.ExposedPorts)
if err != nil {
@ -274,103 +348,30 @@ func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData
}
}
// We need to merge s.Expose into image exposed ports
for k, v := range s.Expose {
expose[k] = v
}
// There's been a request to expose some ports. Let's do that.
// Start by figuring out what needs to be exposed.
// This is a map of container port number to protocols to expose.
toExpose := make(map[uint16][]string)
for port, proto := range expose {
// Validate protocol first
protocols, err := checkProtocol(proto, false)
if err != nil {
return nil, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port)
}
if port == 0 {
return nil, nil, errors.Errorf("cannot expose 0 as it is not a valid port number")
}
// Check to see if the port is already present in existing
// mappings.
for _, p := range protocols {
ctrPortMap, ok := containerPortValidate[p]["0.0.0.0"]
if !ok {
ctrPortMap = make(map[uint16]uint16)
containerPortValidate[p]["0.0.0.0"] = ctrPortMap
toExpose := make(map[uint16][]string, len(s.Expose)+len(expose))
for _, expose := range []map[uint16]string{expose, s.Expose} {
for port, proto := range expose {
if port == 0 {
return nil, nil, errors.Errorf("cannot expose 0 as it is not a valid port number")
}
if portNum := ctrPortMap[port]; portNum == 0 {
// We want to expose this port for this protocol
exposeProto, ok := toExpose[port]
if !ok {
exposeProto = []string{}
}
exposeProto = append(exposeProto, p)
toExpose[port] = exposeProto
protocols, err := checkProtocol(proto, false)
if err != nil {
return nil, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port)
}
toExpose[port] = appendProtocolsNoDuplicates(toExpose[port], protocols)
}
}
// If not publishing exposed ports return mappings and exposed ports.
publishPorts := toExpose
if !s.PublishExposedPorts {
return finalMappings, toExpose, nil
publishPorts = nil
}
// We now have a final list of ports that we want exposed.
// Let's find empty, unallocated host ports for them.
for port, protocols := range toExpose {
for _, p := range protocols {
// Find an open port on the host.
// I see a faint possibility that this will infinite
// loop trying to find a valid open port, so I've
// included a max-tries counter.
hostPort := 0
tries := 15
for hostPort == 0 && tries > 0 {
// We can't select a specific protocol, which is
// unfortunate for the UDP case.
candidate, err := utils.GetRandomPort()
if err != nil {
return nil, nil, err
}
// Check if the host port is already bound
hostPortMap, ok := hostPortValidate[p]["0.0.0.0"]
if !ok {
hostPortMap = make(map[uint16]uint16)
hostPortValidate[p]["0.0.0.0"] = hostPortMap
}
if checkPort := hostPortMap[uint16(candidate)]; checkPort != 0 {
// Host port is already allocated, try again
tries--
continue
}
hostPortMap[uint16(candidate)] = port
hostPort = candidate
logrus.Debugf("Mapping exposed port %d/%s to host port %d", port, p, hostPort)
// Make a CNI port mapping
cniPort := types.OCICNIPortMapping{
HostPort: int32(candidate),
ContainerPort: int32(port),
Protocol: p,
HostIP: "",
}
finalMappings = append(finalMappings, cniPort)
}
if tries == 0 && hostPort == 0 {
// We failed to find an open port.
return nil, nil, errors.Errorf("failed to find an open port to expose container port %d on the host", port)
}
}
finalMappings, err := ParsePortMapping(s.PortMappings, publishPorts)
if err != nil {
return nil, nil, err
}
return finalMappings, nil, nil
return finalMappings, toExpose, nil
}
// Check a string to ensure it is a comma-separated set of valid protocols
@ -409,7 +410,7 @@ func checkProtocol(protocol string, allowSCTP bool) ([]string, error) {
}
func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error) {
expose := make(map[uint16]string)
expose := make(map[uint16]string, len(exposedPorts))
for imgExpose := range exposedPorts {
// Expose format is portNumber[/protocol]
splitExpose := strings.SplitN(imgExpose, "/", 2)
@ -420,12 +421,20 @@ func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error
if num > 65535 || num < 1 {
return nil, errors.Errorf("%d from image EXPOSE statement %q is not a valid port number", num, imgExpose)
}
// No need to validate protocol, we'll do it below.
if len(splitExpose) == 1 {
expose[uint16(num)] = "tcp"
} else {
expose[uint16(num)] = splitExpose[1]
// No need to validate protocol, we'll do it later.
newProto := "tcp"
if len(splitExpose) == 2 {
newProto = splitExpose[1]
}
proto := expose[uint16(num)]
if len(proto) > 1 {
proto = proto + "," + newProto
} else {
proto = newProto
}
expose[uint16(num)] = proto
}
return expose, nil
}

View File

@ -0,0 +1,197 @@
package generate
import (
"fmt"
"testing"
"github.com/containers/podman/v3/libpod/network/types"
)
func benchmarkParsePortMapping(b *testing.B, ports []types.PortMapping) {
for n := 0; n < b.N; n++ {
ParsePortMapping(ports, nil)
}
}
func BenchmarkParsePortMappingNoPorts(b *testing.B) {
benchmarkParsePortMapping(b, nil)
}
func BenchmarkParsePortMapping1(b *testing.B) {
benchmarkParsePortMapping(b, []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
})
}
func BenchmarkParsePortMapping100(b *testing.B) {
ports := make([]types.PortMapping, 0, 100)
for i := uint16(8080); i < 8180; i++ {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMapping1k(b *testing.B) {
ports := make([]types.PortMapping, 0, 1000)
for i := uint16(8080); i < 9080; i++ {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMapping10k(b *testing.B) {
ports := make([]types.PortMapping, 0, 30000)
for i := uint16(8080); i < 18080; i++ {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMapping1m(b *testing.B) {
ports := make([]types.PortMapping, 0, 1000000)
for j := 0; j < 20; j++ {
for i := uint16(1); i <= 50000; i++ {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
HostIP: fmt.Sprintf("192.168.1.%d", j),
})
}
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMappingReverse100(b *testing.B) {
ports := make([]types.PortMapping, 0, 100)
for i := uint16(8180); i > 8080; i-- {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMappingReverse1k(b *testing.B) {
ports := make([]types.PortMapping, 0, 1000)
for i := uint16(9080); i > 8080; i-- {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMappingReverse10k(b *testing.B) {
ports := make([]types.PortMapping, 0, 30000)
for i := uint16(18080); i > 8080; i-- {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
})
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMappingReverse1m(b *testing.B) {
ports := make([]types.PortMapping, 0, 1000000)
for j := 0; j < 20; j++ {
for i := uint16(50000); i > 0; i-- {
ports = append(ports, types.PortMapping{
HostPort: i,
ContainerPort: i,
Protocol: "tcp",
HostIP: fmt.Sprintf("192.168.1.%d", j),
})
}
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}
func BenchmarkParsePortMappingRange1(b *testing.B) {
benchmarkParsePortMapping(b, []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
})
}
func BenchmarkParsePortMappingRange100(b *testing.B) {
benchmarkParsePortMapping(b, []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 100,
},
})
}
func BenchmarkParsePortMappingRange1k(b *testing.B) {
benchmarkParsePortMapping(b, []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1000,
},
})
}
func BenchmarkParsePortMappingRange10k(b *testing.B) {
benchmarkParsePortMapping(b, []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 10000,
},
})
}
func BenchmarkParsePortMappingRange1m(b *testing.B) {
ports := make([]types.PortMapping, 0, 1000000)
for j := 0; j < 20; j++ {
ports = append(ports, types.PortMapping{
HostPort: 1,
ContainerPort: 1,
Protocol: "tcp",
Range: 50000,
HostIP: fmt.Sprintf("192.168.1.%d", j),
})
}
b.ResetTimer()
benchmarkParsePortMapping(b, ports)
}

View File

@ -0,0 +1,989 @@
package generate
import (
"testing"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/stretchr/testify/assert"
)
func TestParsePortMappingWithHostPort(t *testing.T) {
tests := []struct {
name string
arg []types.PortMapping
arg2 map[uint16][]string
want []types.PortMapping
}{
{
name: "no ports",
arg: nil,
want: nil,
},
{
name: "one tcp port",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "one tcp port no proto",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "one udp port",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "udp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "udp",
Range: 1,
},
},
},
{
name: "one sctp port",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "sctp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "sctp",
Range: 1,
},
},
},
{
name: "one port two protocols",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp,udp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "udp",
Range: 1,
},
},
},
{
name: "one port three protocols",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp,udp,sctp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "udp",
Range: 1,
},
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "sctp",
Range: 1,
},
},
},
{
name: "one port with range 1",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "one port with range 5",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 5,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 5,
},
},
},
{
name: "two ports joined",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 2,
},
},
},
{
name: "two ports joined with range",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 2,
},
{
HostPort: 8081,
ContainerPort: 81,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 2,
},
},
},
{
name: "two ports with no overlapping range",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 10,
},
{
HostPort: 9090,
ContainerPort: 9090,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 9090,
ContainerPort: 9090,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 10,
},
},
},
{
name: "four ports with two overlapping ranges",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 10,
},
{
HostPort: 8085,
ContainerPort: 85,
Protocol: "tcp",
Range: 10,
},
{
HostPort: 100,
ContainerPort: 5,
Protocol: "tcp",
},
{
HostPort: 101,
ContainerPort: 6,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 15,
},
{
HostPort: 100,
ContainerPort: 5,
Protocol: "tcp",
Range: 2,
},
},
},
{
name: "two overlapping ranges",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 10,
},
{
HostPort: 8085,
ContainerPort: 85,
Protocol: "tcp",
Range: 2,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 10,
},
},
},
{
name: "four overlapping ranges",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 10,
},
{
HostPort: 8085,
ContainerPort: 85,
Protocol: "tcp",
Range: 2,
},
{
HostPort: 8090,
ContainerPort: 90,
Protocol: "tcp",
Range: 7,
},
{
HostPort: 8095,
ContainerPort: 95,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 17,
},
},
},
{
name: "one port range overlaps 5 ports",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Range: 20,
},
{
HostPort: 8085,
ContainerPort: 85,
Range: 2,
},
{
HostPort: 8090,
ContainerPort: 90,
},
{
HostPort: 8095,
ContainerPort: 95,
},
{
HostPort: 8096,
ContainerPort: 96,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
Range: 20,
},
},
},
{
name: "different host ip same port",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
HostIP: "192.168.1.1",
},
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
HostIP: "192.168.2.1",
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
HostIP: "192.168.1.1",
Range: 1,
},
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
HostIP: "192.168.2.1",
Range: 1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePortMapping(tt.arg, tt.arg2)
assert.NoError(t, err, "error is not nil")
// use ElementsMatch instead of Equal because the order is not consistent
assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping")
})
}
}
func TestParsePortMappingWithoutHostPort(t *testing.T) {
tests := []struct {
name string
arg []types.PortMapping
arg2 map[uint16][]string
want []types.PortMapping
}{
{
name: "one tcp port",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "one port with two protocols",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp,udp",
},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 80,
Protocol: "udp",
Range: 1,
},
},
},
{
name: "same port twice",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "neighbor ports are not joined",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
},
{
HostPort: 0,
ContainerPort: 81,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 81,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "overlapping range ports are joined",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 2,
},
{
HostPort: 0,
ContainerPort: 81,
Protocol: "tcp",
},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 2,
},
},
},
{
name: "four overlapping range ports are joined",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 3,
},
{
HostPort: 0,
ContainerPort: 81,
Protocol: "tcp",
},
{
HostPort: 0,
ContainerPort: 82,
Protocol: "tcp",
Range: 10,
},
{
HostPort: 0,
ContainerPort: 90,
Protocol: "tcp",
Range: 5,
},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 15,
},
},
},
{
name: "expose one tcp port",
arg2: map[uint16][]string{
8080: {"tcp"},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "expose already defined port",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 8080,
Protocol: "tcp",
},
},
arg2: map[uint16][]string{
8080: {"tcp"},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
},
{
name: "expose different proto",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 8080,
Protocol: "tcp",
},
},
arg2: map[uint16][]string{
8080: {"udp"},
},
want: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 8080,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 8080,
Protocol: "udp",
Range: 1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePortMapping(tt.arg, tt.arg2)
assert.NoError(t, err, "error is not nil")
// because we always get random host ports when it is set to 0 we cannot check that exactly
// check if it is not 0 and set to to 0 afterwards
for i := range got {
assert.Greater(t, got[i].HostPort, uint16(0), "host port is zero")
got[i].HostPort = 0
}
// use ElementsMatch instead of Equal because the order is not consistent
assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping")
})
}
}
func TestParsePortMappingMixedHostPort(t *testing.T) {
tests := []struct {
name string
arg []types.PortMapping
want []types.PortMapping
resetHostPorts []int
}{
{
name: "two ports one without a hostport set",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
},
{
HostPort: 8080,
ContainerPort: 8080,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 8080,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
resetHostPorts: []int{1},
},
{
name: "two ports one without a hostport set, inverted order",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 8080,
},
{
HostPort: 0,
ContainerPort: 80,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 8080,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
},
resetHostPorts: []int{1},
},
{
name: "three ports without host ports, one with a hostport set, , inverted order",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
},
{
HostPort: 0,
ContainerPort: 85,
},
{
HostPort: 0,
ContainerPort: 90,
},
{
HostPort: 8080,
ContainerPort: 8080,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 8080,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 85,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 90,
Protocol: "tcp",
Range: 1,
},
},
resetHostPorts: []int{1, 2, 3},
},
{
name: "three ports without host ports, one with a hostport set",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 8080,
},
{
HostPort: 0,
ContainerPort: 90,
},
{
HostPort: 0,
ContainerPort: 85,
},
{
HostPort: 0,
ContainerPort: 80,
},
},
want: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 8080,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 80,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 85,
Protocol: "tcp",
Range: 1,
},
{
HostPort: 0,
ContainerPort: 90,
Protocol: "tcp",
Range: 1,
},
},
resetHostPorts: []int{1, 2, 3},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePortMapping(tt.arg, nil)
assert.NoError(t, err, "error is not nil")
// because we always get random host ports when it is set to 0 we cannot check that exactly
// use resetHostPorts to know which port element is 0
for _, num := range tt.resetHostPorts {
assert.Greater(t, got[num].HostPort, uint16(0), "host port is zero")
got[num].HostPort = 0
}
assert.Equal(t, tt.want, got, "got unexpected port mapping")
})
}
}
func TestParsePortMappingError(t *testing.T) {
tests := []struct {
name string
arg []types.PortMapping
err string
}{
{
name: "container port is 0",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 0,
Protocol: "tcp",
},
},
err: "container port number must be non-0",
},
{
name: "container port range exceeds max",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 65000,
Protocol: "tcp",
Range: 10000,
},
},
err: "container port range exceeds maximum allowable port number",
},
{
name: "host port range exceeds max",
arg: []types.PortMapping{
{
HostPort: 60000,
ContainerPort: 1,
Protocol: "tcp",
Range: 10000,
},
},
err: "host port range exceeds maximum allowable port number",
},
{
name: "invalid protocol",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "1",
},
},
err: "unrecognized protocol \"1\" in port mapping",
},
{
name: "invalid protocol 2",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "udp,u",
},
},
err: "unrecognized protocol \"u\" in port mapping",
},
{
name: "invalid ip address",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
HostIP: "blah",
},
},
err: "invalid IP address \"blah\" in port mapping",
},
{
name: "invalid overalpping range",
arg: []types.PortMapping{
{
HostPort: 8080,
ContainerPort: 80,
Range: 5,
},
{
HostPort: 8081,
ContainerPort: 60,
},
},
err: "conflicting port mappings for host port 8081 (protocol tcp)",
},
{
name: "big port range with host port zero does not fit",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 1,
Range: 65535,
},
},
err: "failed to find an open port to expose container port 1 with range 65535 on the host",
},
{
name: "big port range with host port zero does not fit",
arg: []types.PortMapping{
{
HostPort: 0,
ContainerPort: 80,
Range: 1,
},
{
HostPort: 0,
ContainerPort: 1000,
Range: 64535,
},
},
err: "failed to find an open port to expose container port 1000 with range 64535 on the host",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
_, err := ParsePortMapping(tt.arg, nil)
assert.EqualError(t, err, tt.err, "error does not match")
})
}
}

View File

@ -494,6 +494,23 @@ var _ = Describe("Podman run networking", func() {
Expect(containerConfig[0].NetworkSettings.Ports["80/tcp"][0].HostPort).ToNot(Equal(80))
})
It("podman run forward sctp protocol", func() {
SkipIfRootless("sctp protocol only works as root")
session := podmanTest.Podman([]string{"--log-level=info", "run", "--name=test", "-p", "80/sctp", "-p", "81/sctp", ALPINE})
session.Wait(90)
Expect(session).Should(Exit(0))
// we can only check logrus on local podman
if !IsRemote() {
// check that the info message for sctp protocol is only displayed once
Expect(strings.Count(session.ErrorToString(), "Port reservation for SCTP is not supported")).To(Equal(1), "`Port reservation for SCTP is not supported` is not displayed exactly one time in the logrus logs")
}
results := podmanTest.Podman([]string{"inspect", "test"})
results.Wait(30)
Expect(results).Should(Exit(0))
Expect(results.OutputToString()).To(ContainSubstring(`"80/sctp":`))
Expect(results.OutputToString()).To(ContainSubstring(`"81/sctp":`))
})
It("podman run hostname test", func() {
session := podmanTest.Podman([]string{"run", "--rm", ALPINE, "printenv", "HOSTNAME"})
session.WaitWithDefaultTimeout()

View File

@ -97,6 +97,7 @@ podman \$opts run --name myfailedcontainer --label mylabel=$LABEL_FAILED \
podman \$opts run -d --name myrunningcontainer --label mylabel=$LABEL_RUNNING \
--network bridge \
-p $HOST_PORT:80 \
-p 127.0.0.1:8080-8082:8080-8082 \
-v $pmroot/var/www:/var/www \
-w /var/www \
--mac-address aa:bb:cc:dd:ee:ff \
@ -186,7 +187,7 @@ EOF
is "${lines[1]}" "mycreatedcontainer--Created----$LABEL_CREATED" "created"
is "${lines[2]}" "mydonecontainer--Exited (0).*----<no value>" "done"
is "${lines[3]}" "myfailedcontainer--Exited (17) .*----$LABEL_FAILED" "fail"
is "${lines[4]}" "myrunningcontainer--Up .*--0.0.0.0:$HOST_PORT->80/tcp--$LABEL_RUNNING" "running"
is "${lines[4]}" "myrunningcontainer--Up .*--0\.0\.0\.0:$HOST_PORT->80\/tcp, 127\.0\.0\.1\:8080-8082->8080-8082\/tcp--$LABEL_RUNNING" "running"
# For debugging: dump containers and IDs
if [[ -n "$PODMAN_UPGRADE_TEST_DEBUG" ]]; then