mirror of
https://github.com/containers/podman.git
synced 2025-06-20 17:13:43 +08:00
Merge pull request #6079 from mheon/add_expose
Rework port parsing to support --expose and -P
This commit is contained in:
@ -192,7 +192,6 @@ func getMemoryLimits(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []strin
|
||||
func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error {
|
||||
var (
|
||||
err error
|
||||
// namespaces map[string]string
|
||||
)
|
||||
|
||||
// validate flags as needed
|
||||
@ -234,9 +233,15 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string
|
||||
// We are not handling the Expose flag yet.
|
||||
// s.PortsExpose = c.Expose
|
||||
s.PortMappings = c.Net.PublishPorts
|
||||
s.PublishImagePorts = c.PublishAll
|
||||
s.PublishExposedPorts = c.PublishAll
|
||||
s.Pod = c.Pod
|
||||
|
||||
expose, err := createExpose(c.Expose)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Expose = expose
|
||||
|
||||
for k, v := range map[string]*specgen.Namespace{
|
||||
c.IPC: &s.IpcNS,
|
||||
c.PID: &s.PidNS,
|
||||
|
@ -1,43 +1,201 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/containers/libpod/pkg/specgen"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// createPortBindings iterates ports mappings and exposed ports into a format CNI understands
|
||||
func createPortBindings(ports []string) ([]ocicni.PortMapping, error) {
|
||||
// TODO wants someone to rewrite this code in the future
|
||||
var portBindings []ocicni.PortMapping
|
||||
// The conversion from []string to natBindings is temporary while mheon reworks the port
|
||||
// deduplication code. Eventually that step will not be required.
|
||||
_, natBindings, err := nat.ParsePortSpecs(ports)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for containerPb, hostPb := range natBindings {
|
||||
var pm ocicni.PortMapping
|
||||
pm.ContainerPort = int32(containerPb.Int())
|
||||
for _, i := range hostPb {
|
||||
var hostPort int
|
||||
var err error
|
||||
pm.HostIP = i.HostIP
|
||||
if i.HostPort == "" {
|
||||
hostPort = containerPb.Int()
|
||||
} else {
|
||||
hostPort, err = strconv.Atoi(i.HostPort)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to convert host port to integer")
|
||||
}
|
||||
}
|
||||
// createExpose parses user-provided exposed port definitions and converts them
|
||||
// into SpecGen format.
|
||||
// TODO: The SpecGen format should really handle ranges more sanely - we could
|
||||
// be massively inflating what is sent over the wire with a large range.
|
||||
func createExpose(expose []string) (map[uint16]string, error) {
|
||||
toReturn := make(map[uint16]string)
|
||||
|
||||
pm.HostPort = int32(hostPort)
|
||||
pm.Protocol = containerPb.Proto()
|
||||
portBindings = append(portBindings, pm)
|
||||
for _, e := range expose {
|
||||
// Check for protocol
|
||||
proto := "tcp"
|
||||
splitProto := strings.Split(e, "/")
|
||||
if len(splitProto) > 2 {
|
||||
return nil, errors.Errorf("invalid expose format - protocol can only be specified once")
|
||||
} else if len(splitProto) == 2 {
|
||||
proto = splitProto[1]
|
||||
}
|
||||
|
||||
// Check for a range
|
||||
start, len, err := parseAndValidateRange(splitProto[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var index uint16
|
||||
for index = 0; index < len; index++ {
|
||||
portNum := start + index
|
||||
protocols, ok := toReturn[portNum]
|
||||
if !ok {
|
||||
toReturn[portNum] = proto
|
||||
} else {
|
||||
newProto := strings.Join(append(strings.Split(protocols, ","), strings.Split(proto, ",")...), ",")
|
||||
toReturn[portNum] = newProto
|
||||
}
|
||||
}
|
||||
}
|
||||
return portBindings, nil
|
||||
|
||||
return toReturn, nil
|
||||
}
|
||||
|
||||
// createPortBindings iterates ports mappings into SpecGen format.
|
||||
func createPortBindings(ports []string) ([]specgen.PortMapping, error) {
|
||||
// --publish is formatted as follows:
|
||||
// [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol]
|
||||
toReturn := make([]specgen.PortMapping, 0, len(ports))
|
||||
|
||||
for _, p := range ports {
|
||||
var (
|
||||
ctrPort string
|
||||
proto, hostIP, hostPort *string
|
||||
)
|
||||
|
||||
splitProto := strings.Split(p, "/")
|
||||
switch len(splitProto) {
|
||||
case 1:
|
||||
// No protocol was provided
|
||||
case 2:
|
||||
proto = &(splitProto[1])
|
||||
default:
|
||||
return nil, errors.Errorf("invalid port format - protocol can only be specified once")
|
||||
}
|
||||
|
||||
splitPort := strings.Split(splitProto[0], ":")
|
||||
switch len(splitPort) {
|
||||
case 1:
|
||||
ctrPort = splitPort[0]
|
||||
case 2:
|
||||
hostPort = &(splitPort[0])
|
||||
ctrPort = splitPort[1]
|
||||
case 3:
|
||||
hostIP = &(splitPort[0])
|
||||
hostPort = &(splitPort[1])
|
||||
ctrPort = splitPort[2]
|
||||
default:
|
||||
return nil, errors.Errorf("invalid port format - format is [[hostIP:]hostPort:]containerPort")
|
||||
}
|
||||
|
||||
newPort, err := parseSplitPort(hostIP, hostPort, ctrPort, proto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
toReturn = append(toReturn, newPort)
|
||||
}
|
||||
|
||||
return toReturn, nil
|
||||
}
|
||||
|
||||
// parseSplitPort parses individual components of the --publish flag to produce
|
||||
// a single port mapping in SpecGen format.
|
||||
func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (specgen.PortMapping, error) {
|
||||
newPort := specgen.PortMapping{}
|
||||
if ctrPort == "" {
|
||||
return newPort, errors.Errorf("must provide a non-empty container port to publish")
|
||||
}
|
||||
ctrStart, ctrLen, err := parseAndValidateRange(ctrPort)
|
||||
if err != nil {
|
||||
return newPort, errors.Wrapf(err, "error parsing container port")
|
||||
}
|
||||
newPort.ContainerPort = ctrStart
|
||||
newPort.Range = ctrLen
|
||||
|
||||
if protocol != nil {
|
||||
if *protocol == "" {
|
||||
return newPort, errors.Errorf("must provide a non-empty protocol to publish")
|
||||
}
|
||||
newPort.Protocol = *protocol
|
||||
}
|
||||
if hostIP != nil {
|
||||
if *hostIP == "" {
|
||||
return newPort, errors.Errorf("must provide a non-empty container host IP to publish")
|
||||
}
|
||||
testIP := net.ParseIP(*hostIP)
|
||||
if testIP == nil {
|
||||
return newPort, errors.Errorf("cannot parse %q as an IP address", *hostIP)
|
||||
}
|
||||
newPort.HostIP = testIP.String()
|
||||
}
|
||||
if hostPort != nil {
|
||||
if *hostPort == "" {
|
||||
return newPort, errors.Errorf("must provide a non-empty container host port to publish")
|
||||
}
|
||||
hostStart, hostLen, err := parseAndValidateRange(*hostPort)
|
||||
if err != nil {
|
||||
return newPort, errors.Wrapf(err, "error parsing host port")
|
||||
}
|
||||
if hostLen != ctrLen {
|
||||
return newPort, errors.Errorf("host and container port ranges have different lengths: %d vs %d", hostLen, ctrLen)
|
||||
}
|
||||
newPort.HostPort = hostStart
|
||||
}
|
||||
|
||||
hport := newPort.HostPort
|
||||
if hport == 0 {
|
||||
hport = newPort.ContainerPort
|
||||
}
|
||||
logrus.Debugf("Adding port mapping from %d to %d length %d protocol %q", hport, newPort.ContainerPort, newPort.Range, newPort.Protocol)
|
||||
|
||||
return newPort, nil
|
||||
}
|
||||
|
||||
// Parse and validate a port range.
|
||||
// Returns start port, length of range, error.
|
||||
func parseAndValidateRange(portRange string) (uint16, uint16, error) {
|
||||
splitRange := strings.Split(portRange, "-")
|
||||
if len(splitRange) > 2 {
|
||||
return 0, 0, errors.Errorf("invalid port format - port ranges are formatted as startPort-stopPort")
|
||||
}
|
||||
|
||||
if splitRange[0] == "" {
|
||||
return 0, 0, errors.Errorf("port numbers cannot be negative")
|
||||
}
|
||||
|
||||
startPort, err := parseAndValidatePort(splitRange[0])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
var rangeLen uint16 = 1
|
||||
if len(splitRange) == 2 {
|
||||
if splitRange[1] == "" {
|
||||
return 0, 0, errors.Errorf("must provide ending number for port range")
|
||||
}
|
||||
endPort, err := parseAndValidatePort(splitRange[1])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if endPort <= startPort {
|
||||
return 0, 0, errors.Errorf("the end port of a range must be higher than the start port - %d is not higher than %d", endPort, startPort)
|
||||
}
|
||||
// Our range is the total number of ports
|
||||
// involved, so we need to add 1 (8080:8081 is
|
||||
// 2 ports, for example, not 1)
|
||||
rangeLen = endPort - startPort + 1
|
||||
}
|
||||
|
||||
return startPort, rangeLen, nil
|
||||
}
|
||||
|
||||
// Turn a single string into a valid U16 port.
|
||||
func parseAndValidatePort(port string) (uint16, error) {
|
||||
num, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "cannot parse %q as a port number", port)
|
||||
}
|
||||
if num < 1 || num > 65535 {
|
||||
return 0, errors.Errorf("port numbers must be between 1 and 65535 (inclusive), got %d", num)
|
||||
}
|
||||
return uint16(num), nil
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"github.com/containers/libpod/libpod/events"
|
||||
"github.com/containers/libpod/pkg/specgen"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
@ -40,7 +39,7 @@ type NetOptions struct {
|
||||
DNSServers []net.IP
|
||||
Network specgen.Namespace
|
||||
NoHosts bool
|
||||
PublishPorts []ocicni.PortMapping
|
||||
PublishPorts []specgen.PortMapping
|
||||
StaticIP *net.IP
|
||||
StaticMAC *net.HardwareAddr
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts, err := createContainerOptions(rt, s, pod, finalVolumes)
|
||||
opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, newImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -115,7 +115,7 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
|
||||
return rt.NewContainer(ctx, runtimeSpec, options...)
|
||||
}
|
||||
|
||||
func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume) ([]libpod.CtrCreateOption, error) {
|
||||
func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, img *image.Image) ([]libpod.CtrCreateOption, error) {
|
||||
var options []libpod.CtrCreateOption
|
||||
var err error
|
||||
|
||||
@ -134,7 +134,7 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l
|
||||
options = append(options, rt.WithPod(pod))
|
||||
}
|
||||
destinations := []string{}
|
||||
// // Take all mount and named volume destinations.
|
||||
// Take all mount and named volume destinations.
|
||||
for _, mount := range s.Mounts {
|
||||
destinations = append(destinations, mount.Destination)
|
||||
}
|
||||
@ -188,7 +188,7 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l
|
||||
options = append(options, libpod.WithPrivileged(s.Privileged))
|
||||
|
||||
// Get namespace related options
|
||||
namespaceOptions, err := GenerateNamespaceOptions(s, rt, pod)
|
||||
namespaceOptions, err := GenerateNamespaceOptions(ctx, s, rt, pod, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/libpod/libpod"
|
||||
"github.com/containers/libpod/libpod/define"
|
||||
"github.com/containers/libpod/libpod/image"
|
||||
"github.com/containers/libpod/pkg/rootless"
|
||||
"github.com/containers/libpod/pkg/specgen"
|
||||
"github.com/containers/libpod/pkg/util"
|
||||
@ -76,7 +78,7 @@ func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod)
|
||||
// joining a pod.
|
||||
// TODO: Consider grouping options that are not directly attached to a namespace
|
||||
// elsewhere.
|
||||
func GenerateNamespaceOptions(s *specgen.SpecGenerator, rt *libpod.Runtime, pod *libpod.Pod) ([]libpod.CtrCreateOption, error) {
|
||||
func GenerateNamespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, pod *libpod.Pod, img *image.Image) ([]libpod.CtrCreateOption, error) {
|
||||
toReturn := []libpod.CtrCreateOption{}
|
||||
|
||||
// If pod is not nil, get infra container.
|
||||
@ -204,7 +206,6 @@ func GenerateNamespaceOptions(s *specgen.SpecGenerator, rt *libpod.Runtime, pod
|
||||
}
|
||||
|
||||
// Net
|
||||
// TODO image ports
|
||||
// TODO validate CNINetworks, StaticIP, StaticIPv6 are only set if we
|
||||
// are in bridge mode.
|
||||
postConfigureNetNS := !s.UserNS.IsHost()
|
||||
@ -221,9 +222,17 @@ func GenerateNamespaceOptions(s *specgen.SpecGenerator, rt *libpod.Runtime, pod
|
||||
}
|
||||
toReturn = append(toReturn, libpod.WithNetNSFrom(netCtr))
|
||||
case specgen.Slirp:
|
||||
toReturn = append(toReturn, libpod.WithNetNS(s.PortMappings, postConfigureNetNS, "slirp4netns", nil))
|
||||
portMappings, err := createPortMappings(ctx, s, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
toReturn = append(toReturn, libpod.WithNetNS(portMappings, postConfigureNetNS, "slirp4netns", nil))
|
||||
case specgen.Bridge:
|
||||
toReturn = append(toReturn, libpod.WithNetNS(s.PortMappings, postConfigureNetNS, "bridge", s.CNINetworks))
|
||||
portMappings, err := createPortMappings(ctx, s, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
toReturn = append(toReturn, libpod.WithNetNS(portMappings, postConfigureNetNS, "bridge", s.CNINetworks))
|
||||
}
|
||||
|
||||
if s.UseImageHosts {
|
||||
@ -428,7 +437,7 @@ func specConfigureNamespaces(s *specgen.SpecGenerator, g *generate.Generator, rt
|
||||
if g.Config.Annotations == nil {
|
||||
g.Config.Annotations = make(map[string]string)
|
||||
}
|
||||
if s.PublishImagePorts {
|
||||
if s.PublishExposedPorts {
|
||||
g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue
|
||||
} else {
|
||||
g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse
|
||||
|
@ -83,7 +83,11 @@ func createPodOptions(p *specgen.PodSpecGenerator) ([]libpod.PodCreateOption, er
|
||||
options = append(options, libpod.WithPodUseImageHosts())
|
||||
}
|
||||
if len(p.PortMappings) > 0 {
|
||||
options = append(options, libpod.WithInfraContainerPorts(p.PortMappings))
|
||||
ports, _, _, err := parsePortMapping(p.PortMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options = append(options, libpod.WithInfraContainerPorts(ports))
|
||||
}
|
||||
options = append(options, libpod.WithPodCgroups())
|
||||
return options, nil
|
||||
|
333
pkg/specgen/generate/ports.go
Normal file
333
pkg/specgen/generate/ports.go
Normal file
@ -0,0 +1,333 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/libpod/libpod/image"
|
||||
"github.com/containers/libpod/pkg/specgen"
|
||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
protoTCP = "tcp"
|
||||
protoUDP = "udp"
|
||||
protoSCTP = "sctp"
|
||||
)
|
||||
|
||||
// Parse port maps to OCICNI port mappings.
|
||||
// Returns a set of OCICNI port mappings, and maps of utilized container and
|
||||
// host ports.
|
||||
func parsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping, 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.
|
||||
finalMappings := []ocicni.PortMapping{}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Iterate through all port mappings, generating OCICNI PortMapping
|
||||
// structs and validating there is no overlap.
|
||||
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"
|
||||
}
|
||||
if ip := net.ParseIP(hostIP); ip == nil {
|
||||
return nil, nil, nil, errors.Errorf("invalid IP address %s in port mapping", port.HostIP)
|
||||
}
|
||||
|
||||
// Validate port numbers and range.
|
||||
len := port.Range
|
||||
if len == 0 {
|
||||
len = 1
|
||||
}
|
||||
containerPort := port.ContainerPort
|
||||
if containerPort == 0 {
|
||||
return nil, nil, nil, errors.Errorf("container port number must be non-0")
|
||||
}
|
||||
hostPort := port.HostPort
|
||||
if hostPort == 0 {
|
||||
hostPort = containerPort
|
||||
}
|
||||
if uint32(len-1)+uint32(containerPort) > 65535 {
|
||||
return nil, nil, 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")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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 + index
|
||||
|
||||
if cPort == 0 || hPort == 0 {
|
||||
return nil, nil, nil, errors.Errorf("host and container ports cannot be 0")
|
||||
}
|
||||
|
||||
testCPort := ctrPortMap[cPort]
|
||||
if testCPort != 0 && testCPort != hPort {
|
||||
// This is an attempt to redefine a port
|
||||
return nil, nil, nil, errors.Errorf("conflicting port mappings for container port %d (protocol %s)", cPort, p)
|
||||
}
|
||||
ctrPortMap[cPort] = hPort
|
||||
|
||||
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
|
||||
|
||||
// If we have an exact duplicate, just continue
|
||||
if testCPort == hPort && testHPort == cPort {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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 := ocicni.PortMapping{
|
||||
HostPort: int32(hPort),
|
||||
ContainerPort: int32(cPort),
|
||||
Protocol: p,
|
||||
HostIP: port.HostIP,
|
||||
}
|
||||
finalMappings = append(finalMappings, cniPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalMappings, containerPortValidate, hostPortValidate, nil
|
||||
}
|
||||
|
||||
// Make final port mappings for the container
|
||||
func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, img *image.Image) ([]ocicni.PortMapping, error) {
|
||||
finalMappings, containerPortValidate, hostPortValidate, err := parsePortMapping(s.PortMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If not publishing exposed ports, or if we are publishing and there is
|
||||
// nothing to publish - then just return the port mappings we've made so
|
||||
// far.
|
||||
if !s.PublishExposedPorts || (len(s.Expose) == 0 && img == nil) {
|
||||
return finalMappings, nil
|
||||
}
|
||||
|
||||
logrus.Debugf("Adding exposed ports")
|
||||
|
||||
// We need to merge s.Expose into image exposed ports
|
||||
expose := make(map[uint16]string)
|
||||
for k, v := range s.Expose {
|
||||
expose[k] = v
|
||||
}
|
||||
if img != nil {
|
||||
inspect, err := img.InspectNoSize(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error inspecting image to get exposed ports")
|
||||
}
|
||||
for imgExpose := range inspect.Config.ExposedPorts {
|
||||
// Expose format is portNumber[/protocol]
|
||||
splitExpose := strings.SplitN(imgExpose, "/", 2)
|
||||
num, err := strconv.Atoi(splitExpose[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to convert image EXPOSE statement %q to port number", imgExpose)
|
||||
}
|
||||
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"
|
||||
} else {
|
||||
expose[uint16(num)] = splitExpose[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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, errors.Wrapf(err, "error validating protocols for exposed port %d", port)
|
||||
}
|
||||
|
||||
if port == 0 {
|
||||
return 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := getRandomPort()
|
||||
if err != nil {
|
||||
return 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 := ocicni.PortMapping{
|
||||
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, errors.Errorf("failed to find an open port to expose container port %d on the host", port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalMappings, 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 {
|
||||
switch p {
|
||||
case protoTCP, "":
|
||||
protocols[protoTCP] = struct{}{}
|
||||
case protoUDP:
|
||||
protocols[protoUDP] = struct{}{}
|
||||
case protoSCTP:
|
||||
if !allowSCTP {
|
||||
return nil, errors.Errorf("protocol SCTP is not allowed for exposed ports")
|
||||
}
|
||||
protocols[protoSCTP] = struct{}{}
|
||||
default:
|
||||
return nil, errors.Errorf("unrecognized protocol %q in port mapping", p)
|
||||
}
|
||||
}
|
||||
|
||||
finalProto := []string{}
|
||||
for p := range protocols {
|
||||
finalProto = append(finalProto, p)
|
||||
}
|
||||
|
||||
// This shouldn't be possible, but check anyways
|
||||
if len(finalProto) == 0 {
|
||||
return nil, errors.Errorf("no valid protocols specified for port mapping")
|
||||
}
|
||||
|
||||
return finalProto, nil
|
||||
}
|
||||
|
||||
// Find a random, open port on the host
|
||||
func getRandomPort() (int, error) {
|
||||
l, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "unable to get free TCP port")
|
||||
}
|
||||
defer l.Close()
|
||||
_, randomPort, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "unable to determine free port")
|
||||
}
|
||||
rp, err := strconv.Atoi(randomPort)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "unable to convert random port to int")
|
||||
}
|
||||
return rp, nil
|
||||
}
|
@ -2,8 +2,6 @@ package specgen
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||
)
|
||||
|
||||
// PodBasicConfig contains basic configuration options for pods.
|
||||
@ -79,7 +77,7 @@ type PodNetworkConfig struct {
|
||||
// container, this will forward the ports to the entire pod.
|
||||
// Only available if NetNS is set to Bridge or Slirp.
|
||||
// Optional.
|
||||
PortMappings []ocicni.PortMapping `json:"portmappings,omitempty"`
|
||||
PortMappings []PortMapping `json:"portmappings,omitempty"`
|
||||
// CNINetworks is a list of CNI networks that the infra container will
|
||||
// join. As, by default, containers share their network with the infra
|
||||
// container, these networks will effectively be joined by the
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/storage"
|
||||
"github.com/cri-o/ocicni/pkg/ocicni"
|
||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
@ -306,11 +305,23 @@ type ContainerNetworkConfig struct {
|
||||
// PortBindings is a set of ports to map into the container.
|
||||
// Only available if NetNS is set to bridge or slirp.
|
||||
// Optional.
|
||||
PortMappings []ocicni.PortMapping `json:"portmappings,omitempty"`
|
||||
// PublishImagePorts will publish ports specified in the image to random
|
||||
// ports outside.
|
||||
// Requires Image to be set.
|
||||
PublishImagePorts bool `json:"publish_image_ports,omitempty"`
|
||||
PortMappings []PortMapping `json:"portmappings,omitempty"`
|
||||
// PublishExposedPorts will publish ports specified in the image to
|
||||
// random unused ports (guaranteed to be above 1024) on the host.
|
||||
// This is based on ports set in Expose below, and any ports specified
|
||||
// by the Image (if one is given).
|
||||
// Only available if NetNS is set to Bridge or Slirp.
|
||||
PublishExposedPorts bool `json:"publish_image_ports,omitempty"`
|
||||
// Expose is a number of ports that will be forwarded to the container
|
||||
// if PublishExposedPorts is set.
|
||||
// Expose is a map of uint16 (port number) to a string representing
|
||||
// protocol. Allowed protocols are "tcp", "udp", and "sctp", or some
|
||||
// combination of the three separated by commas.
|
||||
// If protocol is set to "" we will assume TCP.
|
||||
// Only available if NetNS is set to Bridge or Slirp, and
|
||||
// PublishExposedPorts is set.
|
||||
// Optional.
|
||||
Expose map[uint16]string `json:"expose,omitempty"`
|
||||
// CNINetworks is a list of CNI networks to join the container to.
|
||||
// If this list is empty, the default CNI network will be joined
|
||||
// instead. If at least one entry is present, we will not join the
|
||||
@ -410,6 +421,35 @@ type NamedVolume struct {
|
||||
Options []string
|
||||
}
|
||||
|
||||
// PortMapping is one or more ports that will be mapped into the container.
|
||||
type PortMapping struct {
|
||||
// HostIP is the IP that we will bind to on the host.
|
||||
// If unset, assumed to be 0.0.0.0 (all interfaces).
|
||||
HostIP string `json:"host_ip,omitempty"`
|
||||
// ContainerPort is the port number that will be exposed from the
|
||||
// container.
|
||||
// Mandatory.
|
||||
ContainerPort uint16 `json:"container_port"`
|
||||
// HostPort is the port number that will be forwarded from the host into
|
||||
// the container.
|
||||
// If omitted, will be assumed to be identical to
|
||||
HostPort uint16 `json:"host_port,omitempty"`
|
||||
// Range is the number of ports that will be forwarded, starting at
|
||||
// HostPort and ContainerPort and counting up.
|
||||
// This is 1-indexed, so 1 is assumed to be a single port (only the
|
||||
// Hostport:Containerport mapping will be added), 2 is two ports (both
|
||||
// Hostport:Containerport and Hostport+1:Containerport+1), etc.
|
||||
// If unset, assumed to be 1 (a single port).
|
||||
// Both hostport + range and containerport + range must be less than
|
||||
// 65536.
|
||||
Range uint16 `json:"range,omitempty"`
|
||||
// Protocol is the protocol forward.
|
||||
// Must be either "tcp", "udp", and "sctp", or some combination of these
|
||||
// separated by commas.
|
||||
// If unset, assumed to be TCP.
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
}
|
||||
|
||||
// NewSpecGenerator returns a SpecGenerator struct given one of two mandatory inputs
|
||||
func NewSpecGenerator(arg string, rootfs bool) *SpecGenerator {
|
||||
csc := ContainerStorageConfig{}
|
||||
|
@ -19,7 +19,6 @@ var _ = Describe("Podman run networking", func() {
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
Skip(v2fail)
|
||||
tempdir, err = CreateTempDirInTempDir()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
@ -65,6 +64,110 @@ var _ = Describe("Podman run networking", func() {
|
||||
Expect(results.OutputToString()).To(ContainSubstring("223"))
|
||||
})
|
||||
|
||||
It("podman run -p 80", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "-p", "80", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("tcp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal(""))
|
||||
})
|
||||
|
||||
It("podman run -p 8080:80", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "-p", "8080:80", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Equal(int32(8080)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("tcp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal(""))
|
||||
})
|
||||
|
||||
It("podman run -p 80/udp", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "-p", "80/udp", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("udp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal(""))
|
||||
})
|
||||
|
||||
It("podman run -p 127.0.0.1:8080:80", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "-p", "127.0.0.1:8080:80", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Equal(int32(8080)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("tcp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal("127.0.0.1"))
|
||||
})
|
||||
|
||||
It("podman run -p 127.0.0.1:8080:80/udp", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "-p", "127.0.0.1:8080:80/udp", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Equal(int32(8080)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("udp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal("127.0.0.1"))
|
||||
})
|
||||
|
||||
It("podman run --expose 80 -P", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "--expose", "80", "-P", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Not(Equal(int32(0))))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("tcp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal(""))
|
||||
})
|
||||
|
||||
It("podman run --expose 80/udp -P", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "--expose", "80/udp", "-P", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Not(Equal(int32(0))))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("udp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal(""))
|
||||
})
|
||||
|
||||
It("podman run --expose 80 -p 80", func() {
|
||||
name := "testctr"
|
||||
session := podmanTest.Podman([]string{"create", "-t", "--expose", "80", "-p", "80", "--name", name, ALPINE, "/bin/sh"})
|
||||
session.WaitWithDefaultTimeout()
|
||||
inspectOut := podmanTest.InspectContainer(name)
|
||||
Expect(len(inspectOut)).To(Equal(1))
|
||||
Expect(len(inspectOut[0].NetworkSettings.Ports)).To(Equal(1))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].ContainerPort).To(Equal(int32(80)))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].Protocol).To(Equal("tcp"))
|
||||
Expect(inspectOut[0].NetworkSettings.Ports[0].HostIP).To(Equal(""))
|
||||
})
|
||||
|
||||
It("podman run network expose host port 80 to container port 8000", func() {
|
||||
SkipIfRootless()
|
||||
session := podmanTest.Podman([]string{"run", "-dt", "-p", "80:8000", ALPINE, "/bin/sh"})
|
||||
|
Reference in New Issue
Block a user