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 committed 2018-11-20 09:49:34 -06:00
1 parent 4eecc8cf70
commit 690c52a113
9 files changed
+254 -7

No files matched your search

+59
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
}
+4 -2
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="
+9
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.
+11
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
}
}
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"time"
"github.com/containers/storage"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/pkg/errors"
)
@@ -96,7 +97,8 @@ type PodContainerInfo struct {
// InfraContainerConfig is the configuration for the pod's infra container
type InfraContainerConfig struct {
HasInfraContainer bool `json:"makeInfraContainer"`
HasInfraContainer bool `json:"makeInfraContainer"`
PortBindings []ocicni.PortMapping `json:"infraPortBindings"`
}
// ID retrieves the pod's ID
+128
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('}')
}
+1 -3
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...)
}
-1
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 {
+39
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))
})
})