diff options
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 33ea731fd..87d37a9e6 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 e792a410c..d0f56ce95 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" @@ -88,10 +89,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 @@ -1200,9 +1198,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) @@ -1294,9 +1290,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) @@ -1364,16 +1358,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 135b2f363..59795432f 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 5cb302a85..b5e378189 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 \ $IMAGE /bin/busybox-extras httpd -f -p 80 @@ -185,7 +186,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 |