Files
Paul Holzinger f93fcf7dee bump go to 1.22
Many dependencies started using go 1.22 which means we have to follow in
order to update.

Disable the now depracted exportloopref linter as it was replaced by
copyloopvar as go fixed the loop copy problem in 1.22[1]

Another new chnage in go 1.22 is the for loop syntax over ints, the
intrange linter chacks for this but there a lot of loops that have to be
converted so I didn't do it here and disable th elinter for now, th eold
syntax is still fine.

[1] https://go.dev/blog/loopvar-preview

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2024-09-03 15:14:15 +02:00

424 lines
13 KiB
Go

//go:build !remote
package generate
import (
"fmt"
"net"
"slices"
"sort"
"strings"
"github.com/containers/common/libimage"
"github.com/containers/common/libnetwork/types"
"github.com/containers/podman/v5/pkg/specgen"
"github.com/containers/podman/v5/pkg/specgenutil"
"github.com/containers/podman/v5/utils"
"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, fmt.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 an array with the 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, fmt.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, fmt.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, fmt.Errorf("container port number must be non-0")
}
hostPort := port.HostPort
if uint32(portRange-1)+uint32(containerPort) > 65535 {
return nil, fmt.Errorf("container port range exceeds maximum allowable port number")
}
if uint32(portRange-1)+uint32(hostPort) > 65535 {
return nil, fmt.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 {
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)
// Mark this port as used so it doesn't get re-generated
allPorts[p.HostPort] = true
} 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 slices.Contains(slice, proto) {
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, fmt.Errorf("cannot expose 0 as it is not a valid port number")
}
protocols, err := checkProtocol(proto, false)
if err != nil {
return nil, nil, fmt.Errorf("validating protocols for exposed port %d: %w", port, err)
}
toExpose[port] = appendProtocolsNoDuplicates(toExpose[port], protocols)
}
}
publishPorts := toExpose
if s.PublishExposedPorts == nil || !*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, fmt.Errorf("protocol SCTP is not allowed for exposed ports")
}
protocols[protoSCTP] = struct{}{}
default:
return nil, fmt.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, fmt.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, fmt.Errorf("unable to convert image EXPOSE: %w", err)
}
return toReturn, nil
}