mirror of https://github.com/containers/podman.git
422 lines
13 KiB
Go
422 lines
13 KiB
Go
package generate
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/containers/common/libimage"
|
|
"github.com/containers/common/libnetwork/types"
|
|
"github.com/containers/podman/v4/utils"
|
|
|
|
"github.com/containers/podman/v4/pkg/specgen"
|
|
"github.com/containers/podman/v4/pkg/specgenutil"
|
|
"github.com/containers/podman/v4/pkg/util"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
protoTCP = "tcp"
|
|
protoUDP = "udp"
|
|
protoSCTP = "sctp"
|
|
)
|
|
|
|
// 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, exposePorts map[uint16][]string) ([]types.PortMapping, error) {
|
|
if len(portMappings) == 0 && len(exposePorts) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// tempMapping stores the ports without ip and protocol
|
|
type tempMapping struct {
|
|
hostPort uint16
|
|
containerPort uint16
|
|
rangePort uint16
|
|
}
|
|
|
|
// 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)
|
|
|
|
// 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, err
|
|
}
|
|
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.
|
|
portRange := port.Range
|
|
if portRange == 0 {
|
|
portRange = 1
|
|
}
|
|
containerPort := port.ContainerPort
|
|
if containerPort == 0 {
|
|
return nil, errors.Errorf("container port number must be non-0")
|
|
}
|
|
hostPort := port.HostPort
|
|
if uint32(portRange-1)+uint32(containerPort) > 65535 {
|
|
return nil, errors.Errorf("container 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")
|
|
}
|
|
|
|
hostProtoMap, ok := portMap[port.HostIP]
|
|
if !ok {
|
|
hostProtoMap = make(map[string][]tempMapping)
|
|
for _, proto := range []string{protoTCP, protoUDP, protoSCTP} {
|
|
hostProtoMap[proto] = make([]tempMapping, 0)
|
|
}
|
|
portMap[port.HostIP] = hostProtoMap
|
|
}
|
|
|
|
p := tempMapping{
|
|
hostPort: port.HostPort,
|
|
containerPort: port.ContainerPort,
|
|
rangePort: portRange,
|
|
}
|
|
|
|
for _, proto := range protocols {
|
|
hostProtoMap[proto] = append(hostProtoMap[proto], p)
|
|
}
|
|
}
|
|
|
|
// we do no longer need the original port mappings
|
|
// set it to 0 length so we can reuse it to populate
|
|
// the slice again while keeping the underlying capacity
|
|
portMappings = portMappings[:0]
|
|
|
|
for hostIP, protoMap := range portMap {
|
|
for protocol, ports := range protoMap {
|
|
ports := ports
|
|
if len(ports) == 0 {
|
|
continue
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
if previousPort != nil {
|
|
addPortToUsedPorts(&portMappings, &allUsedHostPorts,
|
|
&allUsedContainerPorts, &usedHostPorts, previousPort)
|
|
}
|
|
|
|
// now take care of the hostPort = 0 ports
|
|
previousPort = nil
|
|
for i < len(ports) {
|
|
p := types.PortMapping{
|
|
HostIP: hostIP,
|
|
Protocol: protocol,
|
|
ContainerPort: ports[i].containerPort,
|
|
Range: ports[i].rangePort,
|
|
}
|
|
var err error
|
|
previousPort, err = joinTwoContainerPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts,
|
|
&allUsedContainerPorts, &usedHostPorts, previousPort, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i++
|
|
}
|
|
if previousPort != nil {
|
|
newPort, err := getRandomHostPort(&usedHostPorts, *previousPort)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addPortToUsedPorts(&portMappings, &allUsedHostPorts,
|
|
&allUsedContainerPorts, &usedHostPorts, &newPort)
|
|
}
|
|
|
|
allUsedContainerPortsMap[protocol] = allUsedContainerPorts
|
|
allUsedHostPortsMap[protocol] = allUsedHostPorts
|
|
}
|
|
}
|
|
|
|
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, err
|
|
}
|
|
portMappings = append(portMappings, p)
|
|
} else {
|
|
newProtocols = append(newProtocols, protocol)
|
|
}
|
|
}
|
|
// make sure to delete the key from the map if there are no protocols left
|
|
if len(newProtocols) == 0 {
|
|
delete(exposePorts, port)
|
|
} else {
|
|
exposePorts[port] = newProtocols
|
|
}
|
|
}
|
|
}
|
|
return portMappings, nil
|
|
}
|
|
|
|
func appendProtocolsNoDuplicates(slice []string, protocols []string) []string {
|
|
for _, proto := range protocols {
|
|
if util.StringInSlice(proto, slice) {
|
|
continue
|
|
}
|
|
slice = append(slice, proto)
|
|
}
|
|
return slice
|
|
}
|
|
|
|
// Make final port mappings for the container
|
|
func createPortMappings(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 {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
publishPorts := toExpose
|
|
if !s.PublishExposedPorts {
|
|
publishPorts = nil
|
|
}
|
|
|
|
finalMappings, err := ParsePortMapping(s.PortMappings, publishPorts)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return finalMappings, toExpose, nil
|
|
}
|
|
|
|
// Check a string to ensure it is a comma-separated set of valid protocols
|
|
func checkProtocol(protocol string, allowSCTP bool) ([]string, error) {
|
|
protocols := make(map[string]struct{})
|
|
splitProto := strings.Split(protocol, ",")
|
|
// Don't error on duplicates - just deduplicate
|
|
for _, p := range splitProto {
|
|
p = strings.ToLower(p)
|
|
switch p {
|
|
case protoTCP, "":
|
|
protocols[protoTCP] = struct{}{}
|
|
case protoUDP:
|
|
protocols[protoUDP] = struct{}{}
|
|
case protoSCTP:
|
|
if !allowSCTP {
|
|
return nil, errors.Errorf("protocol SCTP is not allowed for exposed ports")
|
|
}
|
|
protocols[protoSCTP] = struct{}{}
|
|
default:
|
|
return nil, errors.Errorf("unrecognized protocol %q in port mapping", p)
|
|
}
|
|
}
|
|
|
|
finalProto := []string{}
|
|
for p := range protocols {
|
|
finalProto = append(finalProto, p)
|
|
}
|
|
|
|
// This shouldn't be possible, but check anyways
|
|
if len(finalProto) == 0 {
|
|
return nil, errors.Errorf("no valid protocols specified for port mapping")
|
|
}
|
|
|
|
return finalProto, nil
|
|
}
|
|
|
|
func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error) {
|
|
expose := make([]string, 0, len(exposedPorts))
|
|
for e := range exposedPorts {
|
|
expose = append(expose, e)
|
|
}
|
|
toReturn, err := specgenutil.CreateExpose(expose)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to convert image EXPOSE")
|
|
}
|
|
return toReturn, nil
|
|
}
|