summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorOpenShift Merge Robot <openshift-merge-robot@users.noreply.github.com>2021-11-06 10:39:16 +0100
committerGitHub <noreply@github.com>2021-11-06 10:39:16 +0100
commitabbd6c167e8163a711680db80137a0731e06e564 (patch)
treec11c98dc0eeac187c62b74443ba98c5dcb0961f0 /pkg
parent6805befec2ca9b9a816b1efda0bf43a67cee09a7 (diff)
parent0136a66a83d027049da6338f8ce6dfa8052c8ca3 (diff)
downloadpodman-abbd6c167e8163a711680db80137a0731e06e564.tar.gz
podman-abbd6c167e8163a711680db80137a0731e06e564.tar.bz2
podman-abbd6c167e8163a711680db80137a0731e06e564.zip
Merge pull request #11890 from Luap99/ports
libpod: deduplicate ports in db
Diffstat (limited to 'pkg')
-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
8 files changed, 1491 insertions, 296 deletions
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")
+ })
+ }
+}