aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/containers/port.go50
-rw-r--r--cmd/podman/containers/ps.go183
-rw-r--r--cmd/rootlessport/main.go42
-rw-r--r--libpod/boltdb_state_internal.go9
-rw-r--r--libpod/common_test.go4
-rw-r--r--libpod/container.go2
-rw-r--r--libpod/container_config.go12
-rw-r--r--libpod/container_internal.go13
-rw-r--r--libpod/kube.go51
-rw-r--r--libpod/networking_linux.go81
-rw-r--r--libpod/networking_linux_test.go323
-rw-r--r--libpod/networking_slirp4netns.go4
-rw-r--r--libpod/oci_util.go169
-rw-r--r--libpod/options.go17
-rw-r--r--libpod/state_test.go40
-rw-r--r--libpod/util.go22
-rw-r--r--pkg/checkpoint/checkpoint_restore.go2
-rw-r--r--pkg/domain/entities/container_ps.go2
-rw-r--r--pkg/domain/entities/containers.go2
-rw-r--r--pkg/rootlessport/rootlessport_linux.go2
-rw-r--r--pkg/specgen/generate/pod_create.go4
-rw-r--r--pkg/specgen/generate/ports.go589
-rw-r--r--pkg/specgen/generate/ports_bench_test.go197
-rw-r--r--pkg/specgen/generate/ports_test.go989
-rw-r--r--test/e2e/run_networking_test.go17
-rw-r--r--test/upgrade/test-upgrade.bats3
26 files changed, 2181 insertions, 648 deletions
diff --git a/cmd/podman/containers/port.go b/cmd/podman/containers/port.go
index f309390c3..0e582ae52 100644
--- a/cmd/podman/containers/port.go
+++ b/cmd/podman/containers/port.go
@@ -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 != "" {
diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go
index 9687cd5bd..712de327c 100644
--- a/cmd/podman/containers/ps.go
+++ b/cmd/podman/containers/ps.go
@@ -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
- }
-
- // 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)
+ 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)
+ }
}
- 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]
}
diff --git a/cmd/rootlessport/main.go b/cmd/rootlessport/main.go
index feb9f5c06..e691ce2fc 100644
--- a/cmd/rootlessport/main.go
+++ b/cmd/rootlessport/main.go
@@ -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
diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go
index 3e3c17a9e..e71d82736 100644
--- a/libpod/boltdb_state_internal.go
+++ b/libpod/boltdb_state_internal.go
@@ -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
}
diff --git a/libpod/common_test.go b/libpod/common_test.go
index 4662a33bd..67e29c265 100644
--- a/libpod/common_test.go
+++ b/libpod/common_test.go
@@ -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,
},
},
},
diff --git a/libpod/container.go b/libpod/container.go
index 4d15c04c5..86989a02f 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -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)
diff --git a/libpod/container_config.go b/libpod/container_config.go
index 8f803d744..412be835f 100644
--- a/libpod/container_config.go
+++ b/libpod/container_config.go
@@ -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,
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index b9805faa3..19b48e14b 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -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())
}
diff --git a/libpod/kube.go b/libpod/kube.go
index ad5acea78..e0ed911af 100644
--- a/libpod/kube.go
+++ b/libpod/kube.go
@@ -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)
- }
- cp := v1.ContainerPort{
- // Name will not be supported
- HostPort: p.HostPort,
- HostIP: p.HostIP,
- ContainerPort: p.ContainerPort,
- Protocol: protocol,
- }
- containerPorts = append(containerPorts, cp)
+ 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)
+ }
+ }
}
return containerPorts, nil
}
diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go
index 035fb5832..ef261a438 100644
--- a/libpod/networking_linux.go
+++ b/libpod/networking_linux.go
@@ -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 {
+ if len(ports) == 0 {
+ return nil
+ }
+
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,
- })
+
+ // 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
+}
diff --git a/libpod/networking_linux_test.go b/libpod/networking_linux_test.go
new file mode 100644
index 000000000..06bf05723
--- /dev/null
+++ b/libpod/networking_linux_test.go
@@ -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)
+}
diff --git a/libpod/networking_slirp4netns.go b/libpod/networking_slirp4netns.go
index 56e8eca99..760427f22 100644
--- a/libpod/networking_slirp4netns.go
+++ b/libpod/networking_slirp4netns.go
@@ -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 {
diff --git a/libpod/oci_util.go b/libpod/oci_util.go
index c1afc0d20..6d99d5836 100644
--- a/libpod/oci_util.go
+++ b/libpod/oci_util.go
@@ -32,93 +32,108 @@ 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))
- }
- if err != nil {
- return nil, errors.Wrapf(err, "cannot resolve the UDP address")
+ 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)
+ }
}
+ }
+ }
+ return files, nil
+}
- 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)
- }
+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")
+ }
- 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 := "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)
+ }
- 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 "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")
+ }
- 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)
+ 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 files, nil
+ return file, nil
}
func getOCIRuntimeError(runtimeMsg string) error {
diff --git a/libpod/options.go b/libpod/options.go
index f1cf015f8..250b16556 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -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 {
diff --git a/libpod/state_test.go b/libpod/state_test.go
index 4799d7b8d..5c3b0d7f7 100644
--- a/libpod/state_test.go
+++ b/libpod/state_test.go
@@ -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)
+ })
+}
diff --git a/libpod/util.go b/libpod/util.go
index 8f8303ff2..5154a261e 100644
--- a/libpod/util.go
+++ b/libpod/util.go
@@ -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 {
diff --git a/pkg/checkpoint/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go
index f53e31f9b..637f1c0e8 100644
--- a/pkg/checkpoint/checkpoint_restore.go
+++ b/pkg/checkpoint/checkpoint_restore.go
@@ -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
}
diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go
index 58f231a2f..d018d373f 100644
--- a/pkg/domain/entities/container_ps.go
+++ b/pkg/domain/entities/container_ps.go
@@ -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
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index deae85fe1..869c616ea 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -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.
diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go
index 7b9e5bbfa..7e6075789 100644
--- a/pkg/rootlessport/rootlessport_linux.go
+++ b/pkg/rootlessport/rootlessport_linux.go
@@ -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
diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go
index 88ebc7ae3..501bce05d 100644
--- a/pkg/specgen/generate/pod_create.go
+++ b/pkg/specgen/generate/pod_create.go
@@ -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, "":
diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go
index 992b4a8e9..53a5e5697 100644
--- a/pkg/specgen/generate/ports.go
+++ b/pkg/specgen/generate/ports.go
@@ -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.
- type tempMapping struct {
- mapping types.OCICNIPortMapping
- startOfRange bool
- isInRange bool
+func ParsePortMapping(portMappings []types.PortMapping, exposePorts map[uint16][]string) ([]types.PortMapping, error) {
+ if len(portMappings) == 0 && len(exposePorts) == 0 {
+ return nil, nil
}
- 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)
+
+ // tempMapping stores the ports without ip and protocol
+ type tempMapping struct {
+ 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)
+
+ // 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)
- // Iterate through all port mappings, generating OCICNI PortMapping
- // structs and validating there is no overlap.
+ // 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
- }
-
- // Validate host IP
- hostIP := port.HostIP
- if hostIP == "" {
- hostIP = "0.0.0.0"
+ return nil, err
}
- 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
+ hostProtoMap, ok := portMap[port.HostIP]
+ if !ok {
+ hostProtoMap = make(map[string][]tempMapping)
+ for _, proto := range []string{protoTCP, protoUDP, protoSCTP} {
+ hostProtoMap[proto] = make([]tempMapping, 0)
}
- ctrPortMap, ok := ctrIPMap[hostIP]
- if !ok {
- ctrPortMap = make(map[uint16]uint16)
- ctrIPMap[hostIP] = ctrPortMap
- }
-
- // 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
- }
-
- if cPort == 0 {
- return nil, nil, nil, errors.Errorf("container port cannot be 0")
- }
+ portMap[port.HostIP] = hostProtoMap
+ }
- // 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
- }
- }
+ p := tempMapping{
+ hostPort: port.HostPort,
+ containerPort: port.ContainerPort,
+ rangePort: portRange,
+ }
- // 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))
-
- var (
- candidate int
- err error
- )
-
- // Iterate over all
- for _, tmp := range tempMappings {
- p := tmp.mapping
+ // 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]
- 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
+ // 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
+ })
+
+ 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
+ }
}
- ctrPortMap, ok := ctrIPMap[p.HostIP]
- if !ok {
- ctrPortMap = make(map[uint16]uint16)
- ctrIPMap[p.HostIP] = ctrPortMap
+ if previousPort != nil {
+ addPortToUsedPorts(&portMappings, &allUsedHostPorts,
+ &allUsedContainerPorts, &usedHostPorts, previousPort)
}
- // See if container port has been used elsewhere
- if ctrPortMap[uint16(p.ContainerPort)] != 0 {
- // Duplicate definition. Let's not bother
- // including it.
- continue
+ // 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)
}
- // 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()
+ allUsedContainerPortsMap[protocol] = allUsedContainerPorts
+ allUsedHostPortsMap[protocol] = allUsedHostPorts
+ }
+ }
+
+ if len(exposePorts) > 0 {
+ logrus.Debugf("Adding exposed ports")
+
+ 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, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort)
+ return nil, err
}
+ portMappings = append(portMappings, p)
} else {
- candidate++
- }
-
- 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)
- break
+ newProtocols = append(newProtocols, protocol)
}
}
- if p.HostPort == 0 {
- return nil, nil, nil, errors.Errorf("could not find open host port to map container port %d to", p.ContainerPort)
+ // 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
}
- remadeMappings = append(remadeMappings, p)
}
- return remadeMappings, containerPortValidate, hostPortValidate, nil
}
+ return portMappings, nil
+}
- finalMappings := []types.OCICNIPortMapping{}
- for _, m := range tempMappings {
- finalMappings = append(finalMappings, m.mapping)
+func appendProtocolsNoDuplicates(slice []string, protocols []string) []string {
+ for _, proto := range protocols {
+ if util.StringInSlice(proto, slice) {
+ continue
+ }
+ slice = append(slice, proto)
}
-
- return finalMappings, containerPortValidate, hostPortValidate, nil
+ 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"
+
+ // 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 {
- expose[uint16(num)] = splitExpose[1]
+ proto = newProto
}
+ expose[uint16(num)] = proto
}
return expose, nil
}
diff --git a/pkg/specgen/generate/ports_bench_test.go b/pkg/specgen/generate/ports_bench_test.go
new file mode 100644
index 000000000..06f02acda
--- /dev/null
+++ b/pkg/specgen/generate/ports_bench_test.go
@@ -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)
+}
diff --git a/pkg/specgen/generate/ports_test.go b/pkg/specgen/generate/ports_test.go
new file mode 100644
index 000000000..20d5d0166
--- /dev/null
+++ b/pkg/specgen/generate/ports_test.go
@@ -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")
+ })
+ }
+}
diff --git a/test/e2e/run_networking_test.go b/test/e2e/run_networking_test.go
index bdf3ce5d6..c64cfd2d5 100644
--- a/test/e2e/run_networking_test.go
+++ b/test/e2e/run_networking_test.go
@@ -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()
diff --git a/test/upgrade/test-upgrade.bats b/test/upgrade/test-upgrade.bats
index e9910f3d2..f6f32242b 100644
--- a/test/upgrade/test-upgrade.bats
+++ b/test/upgrade/test-upgrade.bats
@@ -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