Allow users to expose ports from the pod to the host

we need to allow users to expose ports to the host for the purposes
of networking, like a webserver.  the port exposure must be done at
the time the pod is created.

strictly speaking, the port exposure occurs on the infra container.

Signed-off-by: baude <bbaude@redhat.com>
This commit is contained in:
baude
2018-11-19 13:20:56 -06:00
parent 4eecc8cf70
commit 690c52a113
9 changed files with 254 additions and 7 deletions

View File

@ -3,11 +3,15 @@ package main
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/rootless"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
@ -58,6 +62,10 @@ var podCreateFlags = []cli.Flag{
Name: "pod-id-file",
Usage: "Write the pod ID to the file",
},
cli.StringSliceFlag{
Name: "publish, p",
Usage: "Publish a container's port, or a range of ports, to the host (default [])",
},
cli.StringFlag{
Name: "share",
Usage: "A comma delimited list of kernel namespaces the pod will share",
@ -102,6 +110,16 @@ func podCreateCmd(c *cli.Context) error {
defer podIdFile.Close()
defer podIdFile.Sync()
}
if len(c.StringSlice("publish")) > 0 {
if !c.BoolT("infra") {
return errors.Errorf("you must have an infra container to publish port bindings to the host")
}
if rootless.IsRootless() {
return errors.Errorf("rootless networking does not allow port binding to the host")
}
}
if !c.BoolT("infra") && c.IsSet("share") && c.String("share") != "none" && c.String("share") != "" {
return errors.Errorf("You cannot share kernel namespaces on the pod level without an infra container")
}
@ -131,6 +149,14 @@ func podCreateCmd(c *cli.Context) error {
options = append(options, nsOptions...)
}
if len(c.StringSlice("publish")) > 0 {
portBindings, err := CreatePortBindings(c.StringSlice("publish"))
if err != nil {
return err
}
options = append(options, libpod.WithInfraContainerPorts(portBindings))
}
// always have containers use pod cgroups
// User Opt out is not yet supported
options = append(options, libpod.WithPodCgroups())
@ -152,3 +178,36 @@ func podCreateCmd(c *cli.Context) error {
return nil
}
// CreatePortBindings iterates ports mappings and exposed ports into a format CNI understands
func CreatePortBindings(ports []string) ([]ocicni.PortMapping, error) {
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")
}
}
pm.HostPort = int32(hostPort)
pm.Protocol = containerPb.Proto()
portBindings = append(portBindings, pm)
}
}
return portBindings, nil
}

View File

@ -2178,12 +2178,14 @@ _podman_pod_create() {
--cgroup-parent
--infra-command
--infra-image
--share
--podidfile
--label-file
--label
-l
--name
--podidfile
--publish
-p
--share
"
local boolean_options="

View File

@ -51,6 +51,15 @@ Assign a name to the pod
Write the pod ID to the file
**-p**, **--publish**=[]
Publish a port or range of ports from the pod to the host
Format: `ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort`
Both hostPort and containerPort can be specified as a range of ports.
When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range.
Use `podman port` to see the actual mapping: `podman port CONTAINER $CONTAINERPORT`
**--share**=""
A comma deliminated list of kernel namespaces to share. If none or "" is specified, no namespaces will be shared. The namespaces to choose from are ipc, net, pid, user, uts.

View File

@ -1295,3 +1295,14 @@ func WithInfraContainer() PodCreateOption {
return nil
}
}
// WithInfraContainerPorts tells the pod to add port bindings to the pause container
func WithInfraContainerPorts(bindings []ocicni.PortMapping) PodCreateOption {
return func(pod *Pod) error {
if pod.valid {
return ErrPodFinalized
}
pod.config.InfraContainer.PortBindings = bindings
return nil
}
}

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/containers/storage"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/pkg/errors"
)
@ -97,6 +98,7 @@ type PodContainerInfo struct {
// InfraContainerConfig is the configuration for the pod's infra container
type InfraContainerConfig struct {
HasInfraContainer bool `json:"makeInfraContainer"`
PortBindings []ocicni.PortMapping `json:"infraPortBindings"`
}
// ID retrieves the pod's ID

View File

@ -6,6 +6,7 @@ package libpod
import (
json "encoding/json"
ocicni "github.com/cri-o/ocicni/pkg/ocicni"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
@ -721,6 +722,29 @@ func easyjsonBe091417DecodeGithubComContainersLibpodLibpod5(in *jlexer.Lexer, ou
switch key {
case "makeInfraContainer":
out.HasInfraContainer = bool(in.Bool())
case "infraPortBindings":
if in.IsNull() {
in.Skip()
out.PortBindings = nil
} else {
in.Delim('[')
if out.PortBindings == nil {
if !in.IsDelim(']') {
out.PortBindings = make([]ocicni.PortMapping, 0, 1)
} else {
out.PortBindings = []ocicni.PortMapping{}
}
} else {
out.PortBindings = (out.PortBindings)[:0]
}
for !in.IsDelim(']') {
var v6 ocicni.PortMapping
easyjsonBe091417DecodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(in, &v6)
out.PortBindings = append(out.PortBindings, v6)
in.WantComma()
}
in.Delim(']')
}
default:
in.SkipRecursive()
}
@ -745,5 +769,109 @@ func easyjsonBe091417EncodeGithubComContainersLibpodLibpod5(out *jwriter.Writer,
}
out.Bool(bool(in.HasInfraContainer))
}
{
const prefix string = ",\"infraPortBindings\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
if in.PortBindings == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v7, v8 := range in.PortBindings {
if v7 > 0 {
out.RawByte(',')
}
easyjsonBe091417EncodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(out, v8)
}
out.RawByte(']')
}
}
out.RawByte('}')
}
func easyjsonBe091417DecodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(in *jlexer.Lexer, out *ocicni.PortMapping) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeString()
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "hostPort":
out.HostPort = int32(in.Int32())
case "containerPort":
out.ContainerPort = int32(in.Int32())
case "protocol":
out.Protocol = string(in.String())
case "hostIP":
out.HostIP = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjsonBe091417EncodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(out *jwriter.Writer, in ocicni.PortMapping) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"hostPort\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Int32(int32(in.HostPort))
}
{
const prefix string = ",\"containerPort\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Int32(int32(in.ContainerPort))
}
{
const prefix string = ",\"protocol\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Protocol))
}
{
const prefix string = ",\"hostIP\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.HostIP))
}
out.RawByte('}')
}

View File

@ -7,7 +7,6 @@ import (
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/rootless"
"github.com/cri-o/ocicni/pkg/ocicni"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
)
@ -50,9 +49,8 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID
options = append(options, withIsInfra())
// Since user namespace sharing is not implemented, we only need to check if it's rootless
portMappings := make([]ocicni.PortMapping, 0)
networks := make([]string, 0)
options = append(options, WithNetNS(portMappings, isRootless, networks))
options = append(options, WithNetNS(p.config.InfraContainer.PortBindings, isRootless, networks))
return r.newContainer(ctx, g.Config, options...)
}

View File

@ -335,7 +335,6 @@ func (c *CreateConfig) GetContainerCreateOptions(runtime *libpod.Runtime) ([]lib
}
options = append(options, runtime.WithPod(pod))
}
if len(c.PortBindings) > 0 {
portBindings, err = c.CreatePortBindings()
if err != nil {

View File

@ -80,4 +80,43 @@ var _ = Describe("Podman pod create", func() {
check.WaitWithDefaultTimeout()
Expect(len(check.OutputToStringArray())).To(Equal(0))
})
It("podman create pod without network portbindings", func() {
name := "test"
session := podmanTest.Podman([]string{"pod", "create", "--name", name})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
pod := session.OutputToString()
webserver := podmanTest.Podman([]string{"run", "--pod", pod, "-dt", nginx})
webserver.WaitWithDefaultTimeout()
Expect(webserver.ExitCode()).To(Equal(0))
check := SystemExec("nc", []string{"-z", "localhost", "80"})
check.WaitWithDefaultTimeout()
Expect(check.ExitCode()).To(Equal(1))
})
It("podman create pod with network portbindings", func() {
name := "test"
session := podmanTest.Podman([]string{"pod", "create", "--name", name, "-p", "80:80"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
pod := session.OutputToString()
webserver := podmanTest.Podman([]string{"run", "--pod", pod, "-dt", nginx})
webserver.WaitWithDefaultTimeout()
Expect(webserver.ExitCode()).To(Equal(0))
check := SystemExec("nc", []string{"-z", "localhost", "80"})
check.WaitWithDefaultTimeout()
Expect(check.ExitCode()).To(Equal(0))
})
It("podman create pod with no infra but portbindings should fail", func() {
name := "test"
session := podmanTest.Podman([]string{"pod", "create", "--infra=false", "--name", name, "-p", "80:80"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(125))
})
})