Merge pull request from baude/enableportbindinginpods

Allow users to expose ports from the pod to the host
This commit is contained in:
OpenShift Merge Robot
2018-11-20 08:53:21 -08:00
committed by GitHub
9 changed files with 254 additions and 7 deletions

@ -3,11 +3,15 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod" "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/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -58,6 +62,10 @@ var podCreateFlags = []cli.Flag{
Name: "pod-id-file", Name: "pod-id-file",
Usage: "Write the pod ID to the 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{ cli.StringFlag{
Name: "share", Name: "share",
Usage: "A comma delimited list of kernel namespaces the pod will 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.Close()
defer podIdFile.Sync() 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") != "" { 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") 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...) 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 // always have containers use pod cgroups
// User Opt out is not yet supported // User Opt out is not yet supported
options = append(options, libpod.WithPodCgroups()) options = append(options, libpod.WithPodCgroups())
@ -152,3 +178,36 @@ func podCreateCmd(c *cli.Context) error {
return nil 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
}

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

@ -51,6 +51,15 @@ Assign a name to the pod
Write the pod ID to the file 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**="" **--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. 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.

@ -1295,3 +1295,14 @@ func WithInfraContainer() PodCreateOption {
return nil 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
}
}

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

@ -6,6 +6,7 @@ package libpod
import ( import (
json "encoding/json" json "encoding/json"
ocicni "github.com/cri-o/ocicni/pkg/ocicni"
easyjson "github.com/mailru/easyjson" easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer" jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter" jwriter "github.com/mailru/easyjson/jwriter"
@ -721,6 +722,29 @@ func easyjsonBe091417DecodeGithubComContainersLibpodLibpod5(in *jlexer.Lexer, ou
switch key { switch key {
case "makeInfraContainer": case "makeInfraContainer":
out.HasInfraContainer = bool(in.Bool()) 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: default:
in.SkipRecursive() in.SkipRecursive()
} }
@ -745,5 +769,109 @@ func easyjsonBe091417EncodeGithubComContainersLibpodLibpod5(out *jwriter.Writer,
} }
out.Bool(bool(in.HasInfraContainer)) 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('}') out.RawByte('}')
} }

@ -7,7 +7,6 @@ import (
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
"github.com/cri-o/ocicni/pkg/ocicni"
spec "github.com/opencontainers/runtime-spec/specs-go" spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate" "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()) options = append(options, withIsInfra())
// Since user namespace sharing is not implemented, we only need to check if it's rootless // 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) 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...) return r.newContainer(ctx, g.Config, options...)
} }

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

@ -80,4 +80,43 @@ var _ = Describe("Podman pod create", func() {
check.WaitWithDefaultTimeout() check.WaitWithDefaultTimeout()
Expect(len(check.OutputToStringArray())).To(Equal(0)) 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))
})
}) })