Pārlūkot izejas kodu

Add support for host port PublishMode in services

Add api/cli support for adding host port PublishMode in services.

Signed-off-by: Jana Radhakrishnan <mrjana@docker.com>
Jana Radhakrishnan 8 gadi atpakaļ
vecāks
revīzija
14ac9f60d0

+ 15 - 0
api/types/swarm/network.go

@@ -31,8 +31,23 @@ type PortConfig struct {
 	TargetPort uint32 `json:",omitempty"`
 	TargetPort uint32 `json:",omitempty"`
 	// PublishedPort is the port on the swarm hosts
 	// PublishedPort is the port on the swarm hosts
 	PublishedPort uint32 `json:",omitempty"`
 	PublishedPort uint32 `json:",omitempty"`
+	// PublishMode is the mode in which port is published
+	PublishMode PortConfigPublishMode `json:",omitempty"`
 }
 }
 
 
+// PortConfigPublishMode represents the mode in which the port is to
+// be published.
+type PortConfigPublishMode string
+
+const (
+	// PortConfigPublishModeIngress is used for ports published
+	// for ingress load balancing using routing mesh.
+	PortConfigPublishModeIngress PortConfigPublishMode = "ingress"
+	// PortConfigPublishModeHost is used for ports published
+	// for direct host level access on the host where the task is running.
+	PortConfigPublishModeHost PortConfigPublishMode = "host"
+)
+
 // PortConfigProtocol represents the protocol of a port.
 // PortConfigProtocol represents the protocol of a port.
 type PortConfigProtocol string
 type PortConfigProtocol string
 
 

+ 7 - 0
api/types/swarm/task.go

@@ -111,6 +111,7 @@ type TaskStatus struct {
 	Message         string          `json:",omitempty"`
 	Message         string          `json:",omitempty"`
 	Err             string          `json:",omitempty"`
 	Err             string          `json:",omitempty"`
 	ContainerStatus ContainerStatus `json:",omitempty"`
 	ContainerStatus ContainerStatus `json:",omitempty"`
+	PortStatus      PortStatus      `json:",omitempty"`
 }
 }
 
 
 // ContainerStatus represents the status of a container.
 // ContainerStatus represents the status of a container.
@@ -119,3 +120,9 @@ type ContainerStatus struct {
 	PID         int    `json:",omitempty"`
 	PID         int    `json:",omitempty"`
 	ExitCode    int    `json:",omitempty"`
 	ExitCode    int    `json:",omitempty"`
 }
 }
+
+// PortStatus represents the port status of a task's host ports whose
+// service has published host ports
+type PortStatus struct {
+	Ports []PortConfig `json:",omitempty"`
+}

+ 3 - 1
cli/command/service/create.go

@@ -40,12 +40,14 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(&opts.constraints, flagConstraint, "Placement constraints")
 	flags.Var(&opts.constraints, flagConstraint, "Placement constraints")
 	flags.Var(&opts.networks, flagNetwork, "Network attachments")
 	flags.Var(&opts.networks, flagNetwork, "Network attachments")
 	flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
 	flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
-	flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
+	flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port")
+	flags.MarkHidden(flagPublish)
 	flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
 	flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
 	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
 	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
 	flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options")
 	flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options")
 	flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
 	flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
 	flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
 	flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
+	flags.Var(&opts.endpoint.expandedPorts, flagPort, "Publish a port")
 
 
 	flags.SetInterspersed(false)
 	flags.SetInterspersed(false)
 	return cmd
 	return cmd

+ 9 - 5
cli/command/service/opts.go

@@ -287,14 +287,15 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
 }
 }
 
 
 type endpointOptions struct {
 type endpointOptions struct {
-	mode  string
-	ports opts.ListOpts
+	mode          string
+	publishPorts  opts.ListOpts
+	expandedPorts opts.PortOpt
 }
 }
 
 
 func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
 func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
 	portConfigs := []swarm.PortConfig{}
 	portConfigs := []swarm.PortConfig{}
 	// We can ignore errors because the format was already validated by ValidatePort
 	// We can ignore errors because the format was already validated by ValidatePort
-	ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
+	ports, portBindings, _ := nat.ParsePortSpecs(e.publishPorts.GetAll())
 
 
 	for port := range ports {
 	for port := range ports {
 		portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...)
 		portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...)
@@ -302,7 +303,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
 
 
 	return &swarm.EndpointSpec{
 	return &swarm.EndpointSpec{
 		Mode:  swarm.ResolutionMode(strings.ToLower(e.mode)),
 		Mode:  swarm.ResolutionMode(strings.ToLower(e.mode)),
-		Ports: portConfigs,
+		Ports: append(portConfigs, e.expandedPorts.Value()...),
 	}
 	}
 }
 }
 
 
@@ -459,7 +460,7 @@ func newServiceOptions() *serviceOptions {
 		env:             opts.NewListOpts(runconfigopts.ValidateEnv),
 		env:             opts.NewListOpts(runconfigopts.ValidateEnv),
 		envFile:         opts.NewListOpts(nil),
 		envFile:         opts.NewListOpts(nil),
 		endpoint: endpointOptions{
 		endpoint: endpointOptions{
-			ports: opts.NewListOpts(ValidatePort),
+			publishPorts: opts.NewListOpts(ValidatePort),
 		},
 		},
 		groups:    opts.NewListOpts(nil),
 		groups:    opts.NewListOpts(nil),
 		logDriver: newLogDriverOptions(),
 		logDriver: newLogDriverOptions(),
@@ -647,6 +648,9 @@ const (
 	flagPublish               = "publish"
 	flagPublish               = "publish"
 	flagPublishRemove         = "publish-rm"
 	flagPublishRemove         = "publish-rm"
 	flagPublishAdd            = "publish-add"
 	flagPublishAdd            = "publish-add"
+	flagPort                  = "port"
+	flagPortAdd               = "port-add"
+	flagPortRemove            = "port-rm"
 	flagReplicas              = "replicas"
 	flagReplicas              = "replicas"
 	flagReserveCPU            = "reserve-cpu"
 	flagReserveCPU            = "reserve-cpu"
 	flagReserveMemory         = "reserve-memory"
 	flagReserveMemory         = "reserve-memory"

+ 52 - 3
cli/command/service/update.go

@@ -48,6 +48,8 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key")
 	flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key")
 	flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path")
 	flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path")
 	flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port")
 	flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port")
+	flags.MarkHidden(flagPublishRemove)
+	flags.Var(newListOptsVar(), flagPortRemove, "Remove a port(target-port mandatory)")
 	flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
 	flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
 	flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server")
 	flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server")
 	flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option")
 	flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option")
@@ -60,7 +62,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service")
 	flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service")
 	flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service")
 	flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service")
 	flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint")
 	flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint")
-	flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port")
+	flags.Var(&opts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port")
+	flags.MarkHidden(flagPublishAdd)
+	flags.Var(&opts.endpoint.expandedPorts, flagPortAdd, "Add or update a port")
 	flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container")
 	flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container")
 	flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server")
 	flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server")
 	flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option")
 	flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option")
@@ -267,7 +271,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
 		}
 		}
 	}
 	}
 
 
-	if anyChanged(flags, flagPublishAdd, flagPublishRemove) {
+	if anyChanged(flags, flagPublishAdd, flagPublishRemove, flagPortAdd, flagPortRemove) {
 		if spec.EndpointSpec == nil {
 		if spec.EndpointSpec == nil {
 			spec.EndpointSpec = &swarm.EndpointSpec{}
 			spec.EndpointSpec = &swarm.EndpointSpec{}
 		}
 		}
@@ -627,7 +631,13 @@ func portConfigToString(portConfig *swarm.PortConfig) string {
 	if protocol == "" {
 	if protocol == "" {
 		protocol = "tcp"
 		protocol = "tcp"
 	}
 	}
-	return fmt.Sprintf("%v/%s", portConfig.PublishedPort, protocol)
+
+	mode := portConfig.PublishMode
+	if mode == "" {
+		mode = "ingress"
+	}
+
+	return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode)
 }
 }
 
 
 func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
 func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
@@ -649,6 +659,15 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
 		}
 		}
 	}
 	}
 
 
+	if flags.Changed(flagPortAdd) {
+		for _, entry := range flags.Lookup(flagPortAdd).Value.(*opts.PortOpt).Value() {
+			if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry {
+				return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol)
+			}
+			portSet[portConfigToString(&entry)] = entry
+		}
+	}
+
 	// Override previous PortConfig in service if there is any duplicate
 	// Override previous PortConfig in service if there is any duplicate
 	for _, entry := range *portConfig {
 	for _, entry := range *portConfig {
 		if _, ok := portSet[portConfigToString(&entry)]; !ok {
 		if _, ok := portSet[portConfigToString(&entry)]; !ok {
@@ -657,6 +676,14 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
 	}
 	}
 
 
 	toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll()
 	toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll()
+	removePortCSV := flags.Lookup(flagPortRemove).Value.(*opts.ListOpts).GetAll()
+	removePortOpts := &opts.PortOpt{}
+	for _, portCSV := range removePortCSV {
+		if err := removePortOpts.Set(portCSV); err != nil {
+			return err
+		}
+	}
+
 	newPorts := []swarm.PortConfig{}
 	newPorts := []swarm.PortConfig{}
 portLoop:
 portLoop:
 	for _, port := range portSet {
 	for _, port := range portSet {
@@ -666,14 +693,36 @@ portLoop:
 				continue portLoop
 				continue portLoop
 			}
 			}
 		}
 		}
+
+		for _, pConfig := range removePortOpts.Value() {
+			if equalProtocol(port.Protocol, pConfig.Protocol) &&
+				port.TargetPort == pConfig.TargetPort &&
+				equalPublishMode(port.PublishMode, pConfig.PublishMode) {
+				continue portLoop
+			}
+		}
+
 		newPorts = append(newPorts, port)
 		newPorts = append(newPorts, port)
 	}
 	}
+
 	// Sort the PortConfig to avoid unnecessary updates
 	// Sort the PortConfig to avoid unnecessary updates
 	sort.Sort(byPortConfig(newPorts))
 	sort.Sort(byPortConfig(newPorts))
 	*portConfig = newPorts
 	*portConfig = newPorts
 	return nil
 	return nil
 }
 }
 
 
+func equalProtocol(prot1, prot2 swarm.PortConfigProtocol) bool {
+	return prot1 == prot2 ||
+		(prot1 == swarm.PortConfigProtocol("") && prot2 == swarm.PortConfigProtocolTCP) ||
+		(prot2 == swarm.PortConfigProtocol("") && prot1 == swarm.PortConfigProtocolTCP)
+}
+
+func equalPublishMode(mode1, mode2 swarm.PortConfigPublishMode) bool {
+	return mode1 == mode2 ||
+		(mode1 == swarm.PortConfigPublishMode("") && mode2 == swarm.PortConfigPublishModeIngress) ||
+		(mode2 == swarm.PortConfigPublishMode("") && mode1 == swarm.PortConfigPublishModeIngress)
+}
+
 func equalPort(targetPort nat.Port, port swarm.PortConfig) bool {
 func equalPort(targetPort nat.Port, port swarm.PortConfig) bool {
 	return (string(port.Protocol) == targetPort.Proto() &&
 	return (string(port.Protocol) == targetPort.Proto() &&
 		port.TargetPort == uint32(targetPort.Int()))
 		port.TargetPort == uint32(targetPort.Int()))

+ 2 - 16
cli/command/service/update_test.go

@@ -238,7 +238,7 @@ func TestUpdatePortsDuplicateEntries(t *testing.T) {
 func TestUpdatePortsDuplicateKeys(t *testing.T) {
 func TestUpdatePortsDuplicateKeys(t *testing.T) {
 	// Test case for #25375
 	// Test case for #25375
 	flags := newUpdateCommand(nil).Flags()
 	flags := newUpdateCommand(nil).Flags()
-	flags.Set("publish-add", "80:20")
+	flags.Set("publish-add", "80:80")
 
 
 	portConfigs := []swarm.PortConfig{
 	portConfigs := []swarm.PortConfig{
 		{TargetPort: 80, PublishedPort: 80},
 		{TargetPort: 80, PublishedPort: 80},
@@ -247,21 +247,7 @@ func TestUpdatePortsDuplicateKeys(t *testing.T) {
 	err := updatePorts(flags, &portConfigs)
 	err := updatePorts(flags, &portConfigs)
 	assert.Equal(t, err, nil)
 	assert.Equal(t, err, nil)
 	assert.Equal(t, len(portConfigs), 1)
 	assert.Equal(t, len(portConfigs), 1)
-	assert.Equal(t, portConfigs[0].TargetPort, uint32(20))
-}
-
-func TestUpdatePortsConflictingFlags(t *testing.T) {
-	// Test case for #25375
-	flags := newUpdateCommand(nil).Flags()
-	flags.Set("publish-add", "80:80")
-	flags.Set("publish-add", "80:20")
-
-	portConfigs := []swarm.PortConfig{
-		{TargetPort: 80, PublishedPort: 80},
-	}
-
-	err := updatePorts(flags, &portConfigs)
-	assert.Error(t, err, "conflicting port mapping")
+	assert.Equal(t, portConfigs[0].TargetPort, uint32(80))
 }
 }
 
 
 func TestUpdateHealthcheckTable(t *testing.T) {
 func TestUpdateHealthcheckTable(t *testing.T) {

+ 18 - 2
cli/command/task/print.go

@@ -17,10 +17,25 @@ import (
 )
 )
 
 
 const (
 const (
-	psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\n"
+	psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n"
 	maxErrLength  = 30
 	maxErrLength  = 30
 )
 )
 
 
+type portStatus swarm.PortStatus
+
+func (ps portStatus) String() string {
+	if len(ps.Ports) == 0 {
+		return ""
+	}
+
+	str := fmt.Sprintf("*:%d->%d/%s", ps.Ports[0].PublishedPort, ps.Ports[0].TargetPort, ps.Ports[0].Protocol)
+	for _, pConfig := range ps.Ports[1:] {
+		str += fmt.Sprintf(",*:%d->%d/%s", pConfig.PublishedPort, pConfig.TargetPort, pConfig.Protocol)
+	}
+
+	return str
+}
+
 type tasksBySlot []swarm.Task
 type tasksBySlot []swarm.Task
 
 
 func (t tasksBySlot) Len() int {
 func (t tasksBySlot) Len() int {
@@ -51,7 +66,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task
 
 
 	// Ignore flushing errors
 	// Ignore flushing errors
 	defer writer.Flush()
 	defer writer.Flush()
-	fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR"}, "\t"))
+	fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t"))
 
 
 	if err := print(writer, ctx, tasks, resolver, noTrunc); err != nil {
 	if err := print(writer, ctx, tasks, resolver, noTrunc); err != nil {
 		return err
 		return err
@@ -113,6 +128,7 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr
 			command.PrettyPrint(task.Status.State),
 			command.PrettyPrint(task.Status.State),
 			strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))),
 			strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))),
 			taskErr,
 			taskErr,
+			portStatus(task.Status.PortStatus),
 		)
 		)
 	}
 	}
 	return nil
 	return nil

+ 2 - 0
daemon/cluster/convert/network.go

@@ -93,6 +93,7 @@ func endpointSpecFromGRPC(es *swarmapi.EndpointSpec) *types.EndpointSpec {
 			endpointSpec.Ports = append(endpointSpec.Ports, types.PortConfig{
 			endpointSpec.Ports = append(endpointSpec.Ports, types.PortConfig{
 				Name:          portState.Name,
 				Name:          portState.Name,
 				Protocol:      types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])),
 				Protocol:      types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])),
+				PublishMode:   types.PortConfigPublishMode(strings.ToLower(swarmapi.PortConfig_PublishMode_name[int32(portState.PublishMode)])),
 				TargetPort:    portState.TargetPort,
 				TargetPort:    portState.TargetPort,
 				PublishedPort: portState.PublishedPort,
 				PublishedPort: portState.PublishedPort,
 			})
 			})
@@ -112,6 +113,7 @@ func endpointFromGRPC(e *swarmapi.Endpoint) types.Endpoint {
 			endpoint.Ports = append(endpoint.Ports, types.PortConfig{
 			endpoint.Ports = append(endpoint.Ports, types.PortConfig{
 				Name:          portState.Name,
 				Name:          portState.Name,
 				Protocol:      types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])),
 				Protocol:      types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])),
+				PublishMode:   types.PortConfigPublishMode(strings.ToLower(swarmapi.PortConfig_PublishMode_name[int32(portState.PublishMode)])),
 				TargetPort:    portState.TargetPort,
 				TargetPort:    portState.TargetPort,
 				PublishedPort: portState.PublishedPort,
 				PublishedPort: portState.PublishedPort,
 			})
 			})

+ 1 - 0
daemon/cluster/convert/service.go

@@ -199,6 +199,7 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
 			spec.Endpoint.Ports = append(spec.Endpoint.Ports, &swarmapi.PortConfig{
 			spec.Endpoint.Ports = append(spec.Endpoint.Ports, &swarmapi.PortConfig{
 				Name:          portConfig.Name,
 				Name:          portConfig.Name,
 				Protocol:      swarmapi.PortConfig_Protocol(swarmapi.PortConfig_Protocol_value[strings.ToUpper(string(portConfig.Protocol))]),
 				Protocol:      swarmapi.PortConfig_Protocol(swarmapi.PortConfig_Protocol_value[strings.ToUpper(string(portConfig.Protocol))]),
+				PublishMode:   swarmapi.PortConfig_PublishMode(swarmapi.PortConfig_PublishMode_value[strings.ToUpper(string(portConfig.PublishMode))]),
 				TargetPort:    portConfig.TargetPort,
 				TargetPort:    portConfig.TargetPort,
 				PublishedPort: portConfig.PublishedPort,
 				PublishedPort: portConfig.PublishedPort,
 			})
 			})

+ 14 - 0
daemon/cluster/convert/task.go

@@ -63,5 +63,19 @@ func TaskFromGRPC(t swarmapi.Task) types.Task {
 		task.NetworksAttachments = append(task.NetworksAttachments, networkAttachementFromGRPC(na))
 		task.NetworksAttachments = append(task.NetworksAttachments, networkAttachementFromGRPC(na))
 	}
 	}
 
 
+	if t.Status.PortStatus == nil {
+		return task
+	}
+
+	for _, p := range t.Status.PortStatus.Ports {
+		task.Status.PortStatus.Ports = append(task.Status.PortStatus.Ports, types.PortConfig{
+			Name:          p.Name,
+			Protocol:      types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(p.Protocol)])),
+			PublishMode:   types.PortConfigPublishMode(strings.ToLower(swarmapi.PortConfig_PublishMode_name[int32(p.PublishMode)])),
+			TargetPort:    p.TargetPort,
+			PublishedPort: p.PublishedPort,
+		})
+	}
+
 	return task
 	return task
 }
 }

+ 64 - 13
daemon/cluster/executor/container/container.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net"
 	"net"
+	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -17,6 +18,7 @@ import (
 	volumetypes "github.com/docker/docker/api/types/volume"
 	volumetypes "github.com/docker/docker/api/types/volume"
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
 	"github.com/docker/docker/reference"
 	"github.com/docker/docker/reference"
+	"github.com/docker/go-connections/nat"
 	"github.com/docker/swarmkit/agent/exec"
 	"github.com/docker/swarmkit/agent/exec"
 	"github.com/docker/swarmkit/api"
 	"github.com/docker/swarmkit/api"
 	"github.com/docker/swarmkit/protobuf/ptypes"
 	"github.com/docker/swarmkit/protobuf/ptypes"
@@ -136,17 +138,61 @@ func (c *containerConfig) image() string {
 	return reference.WithDefaultTag(ref).String()
 	return reference.WithDefaultTag(ref).String()
 }
 }
 
 
+func (c *containerConfig) portBindings() nat.PortMap {
+	portBindings := nat.PortMap{}
+	if c.task.Endpoint == nil {
+		return portBindings
+	}
+
+	for _, portConfig := range c.task.Endpoint.Ports {
+		if portConfig.PublishMode != api.PublishModeHost {
+			continue
+		}
+
+		port := nat.Port(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String())))
+		binding := []nat.PortBinding{
+			{},
+		}
+
+		if portConfig.PublishedPort != 0 {
+			binding[0].HostPort = strconv.Itoa(int(portConfig.PublishedPort))
+		}
+		portBindings[port] = binding
+	}
+
+	return portBindings
+}
+
+func (c *containerConfig) exposedPorts() map[nat.Port]struct{} {
+	exposedPorts := make(map[nat.Port]struct{})
+	if c.task.Endpoint == nil {
+		return exposedPorts
+	}
+
+	for _, portConfig := range c.task.Endpoint.Ports {
+		if portConfig.PublishMode != api.PublishModeHost {
+			continue
+		}
+
+		port := nat.Port(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String())))
+		exposedPorts[port] = struct{}{}
+	}
+
+	return exposedPorts
+}
+
 func (c *containerConfig) config() *enginecontainer.Config {
 func (c *containerConfig) config() *enginecontainer.Config {
 	config := &enginecontainer.Config{
 	config := &enginecontainer.Config{
-		Labels:      c.labels(),
-		Tty:         c.spec().TTY,
-		User:        c.spec().User,
-		Env:         c.spec().Env,
-		Hostname:    c.spec().Hostname,
-		WorkingDir:  c.spec().Dir,
-		Image:       c.image(),
-		Volumes:     c.volumes(),
-		Healthcheck: c.healthcheck(),
+		Labels:       c.labels(),
+		Tty:          c.spec().TTY,
+		User:         c.spec().User,
+		Env:          c.spec().Env,
+		Hostname:     c.spec().Hostname,
+		WorkingDir:   c.spec().Dir,
+		Image:        c.image(),
+		Volumes:      c.volumes(),
+		ExposedPorts: c.exposedPorts(),
+		Healthcheck:  c.healthcheck(),
 	}
 	}
 
 
 	if len(c.spec().Command) > 0 {
 	if len(c.spec().Command) > 0 {
@@ -333,10 +379,11 @@ func getMountMask(m *api.Mount) string {
 
 
 func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
 func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
 	hc := &enginecontainer.HostConfig{
 	hc := &enginecontainer.HostConfig{
-		Resources: c.resources(),
-		Binds:     c.binds(),
-		Tmpfs:     c.tmpfs(),
-		GroupAdd:  c.spec().Groups,
+		Resources:    c.resources(),
+		Binds:        c.binds(),
+		Tmpfs:        c.tmpfs(),
+		GroupAdd:     c.spec().Groups,
+		PortBindings: c.portBindings(),
 	}
 	}
 
 
 	if c.spec().DNSConfig != nil {
 	if c.spec().DNSConfig != nil {
@@ -525,6 +572,10 @@ func (c *containerConfig) serviceConfig() *clustertypes.ServiceConfig {
 
 
 	if c.task.Endpoint != nil {
 	if c.task.Endpoint != nil {
 		for _, ePort := range c.task.Endpoint.Ports {
 		for _, ePort := range c.task.Endpoint.Ports {
+			if ePort.PublishMode != api.PublishModeIngress {
+				continue
+			}
+
 			svcCfg.ExposedPorts = append(svcCfg.ExposedPorts, &clustertypes.PortConfig{
 			svcCfg.ExposedPorts = append(svcCfg.ExposedPorts, &clustertypes.PortConfig{
 				Name:          ePort.Name,
 				Name:          ePort.Name,
 				Protocol:      int32(ePort.Protocol),
 				Protocol:      int32(ePort.Protocol),

+ 74 - 0
daemon/cluster/executor/container/controller.go

@@ -7,11 +7,14 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"os"
 	"os"
+	"strconv"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/events"
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
+	"github.com/docker/go-connections/nat"
 	"github.com/docker/libnetwork"
 	"github.com/docker/libnetwork"
 	"github.com/docker/swarmkit/agent/exec"
 	"github.com/docker/swarmkit/agent/exec"
 	"github.com/docker/swarmkit/api"
 	"github.com/docker/swarmkit/api"
@@ -69,6 +72,19 @@ func (r *controller) ContainerStatus(ctx context.Context) (*api.ContainerStatus,
 	return parseContainerStatus(ctnr)
 	return parseContainerStatus(ctnr)
 }
 }
 
 
+func (r *controller) PortStatus(ctx context.Context) (*api.PortStatus, error) {
+	ctnr, err := r.adapter.inspect(ctx)
+	if err != nil {
+		if isUnknownContainer(err) {
+			return nil, nil
+		}
+
+		return nil, err
+	}
+
+	return parsePortStatus(ctnr)
+}
+
 // Update tasks a recent task update and applies it to the container.
 // Update tasks a recent task update and applies it to the container.
 func (r *controller) Update(ctx context.Context, t *api.Task) error {
 func (r *controller) Update(ctx context.Context, t *api.Task) error {
 	// TODO(stevvooe): While assignment of tasks is idempotent, we do allow
 	// TODO(stevvooe): While assignment of tasks is idempotent, we do allow
@@ -553,6 +569,64 @@ func parseContainerStatus(ctnr types.ContainerJSON) (*api.ContainerStatus, error
 	return status, nil
 	return status, nil
 }
 }
 
 
+func parsePortStatus(ctnr types.ContainerJSON) (*api.PortStatus, error) {
+	status := &api.PortStatus{}
+
+	if ctnr.NetworkSettings != nil && len(ctnr.NetworkSettings.Ports) > 0 {
+		exposedPorts, err := parsePortMap(ctnr.NetworkSettings.Ports)
+		if err != nil {
+			return nil, err
+		}
+		status.Ports = exposedPorts
+	}
+
+	return status, nil
+}
+
+func parsePortMap(portMap nat.PortMap) ([]*api.PortConfig, error) {
+	exposedPorts := make([]*api.PortConfig, 0, len(portMap))
+
+	for portProtocol, mapping := range portMap {
+		parts := strings.SplitN(string(portProtocol), "/", 2)
+		if len(parts) != 2 {
+			return nil, fmt.Errorf("invalid port mapping: %s", portProtocol)
+		}
+
+		port, err := strconv.ParseUint(parts[0], 10, 16)
+		if err != nil {
+			return nil, err
+		}
+
+		protocol := api.ProtocolTCP
+		switch strings.ToLower(parts[1]) {
+		case "tcp":
+			protocol = api.ProtocolTCP
+		case "udp":
+			protocol = api.ProtocolUDP
+		default:
+			return nil, fmt.Errorf("invalid protocol: %s", parts[1])
+		}
+
+		for _, binding := range mapping {
+			hostPort, err := strconv.ParseUint(binding.HostPort, 10, 16)
+			if err != nil {
+				return nil, err
+			}
+
+			// TODO(aluzzardi): We're losing the port `name` here since
+			// there's no way to retrieve it back from the Engine.
+			exposedPorts = append(exposedPorts, &api.PortConfig{
+				PublishMode:   api.PublishModeHost,
+				Protocol:      protocol,
+				TargetPort:    uint32(port),
+				PublishedPort: uint32(hostPort),
+			})
+		}
+	}
+
+	return exposedPorts, nil
+}
+
 type exitError struct {
 type exitError struct {
 	code  int
 	code  int
 	cause error
 	cause error

+ 1 - 0
integration-cli/docker_cli_service_update_test.go

@@ -32,6 +32,7 @@ func (s *DockerSwarmSuite) TestServiceUpdatePort(c *check.C) {
 			Protocol:      "tcp",
 			Protocol:      "tcp",
 			PublishedPort: 8082,
 			PublishedPort: 8082,
 			TargetPort:    8083,
 			TargetPort:    8083,
+			PublishMode:   "ingress",
 		},
 		},
 	}
 	}
 
 

+ 1 - 4
integration-cli/docker_cli_swarm_test.go

@@ -321,12 +321,9 @@ func (s *DockerSwarmSuite) TestSwarmPublishAdd(c *check.C) {
 	out, err = d.cmdRetryOutOfSequence("service", "update", "--publish-add", "80:80", "--publish-add", "80:20", name)
 	out, err = d.cmdRetryOutOfSequence("service", "update", "--publish-add", "80:80", "--publish-add", "80:20", name)
 	c.Assert(err, checker.NotNil)
 	c.Assert(err, checker.NotNil)
 
 
-	out, err = d.cmdRetryOutOfSequence("service", "update", "--publish-add", "80:20", name)
-	c.Assert(err, checker.IsNil)
-
 	out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", name)
 	out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", name)
 	c.Assert(err, checker.IsNil)
 	c.Assert(err, checker.IsNil)
-	c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 20 80}]")
+	c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 80 80 ingress}]")
 }
 }
 
 
 func (s *DockerSwarmSuite) TestSwarmServiceWithGroup(c *check.C) {
 func (s *DockerSwarmSuite) TestSwarmServiceWithGroup(c *check.C) {

+ 99 - 0
opts/port.go

@@ -0,0 +1,99 @@
+package opts
+
+import (
+	"encoding/csv"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/docker/docker/api/types/swarm"
+)
+
+const (
+	portOptTargetPort    = "target"
+	portOptPublishedPort = "published"
+	portOptProtocol      = "protocol"
+	portOptMode          = "mode"
+)
+
+type PortOpt struct {
+	ports []swarm.PortConfig
+}
+
+// Set a new port value
+func (p *PortOpt) Set(value string) error {
+	csvReader := csv.NewReader(strings.NewReader(value))
+	fields, err := csvReader.Read()
+	if err != nil {
+		return err
+	}
+
+	pConfig := swarm.PortConfig{}
+	for _, field := range fields {
+		parts := strings.SplitN(field, "=", 2)
+		if len(parts) != 2 {
+			return fmt.Errorf("invalid field %s", field)
+		}
+
+		key := strings.ToLower(parts[0])
+		value := strings.ToLower(parts[1])
+
+		switch key {
+		case portOptProtocol:
+			if value != string(swarm.PortConfigProtocolTCP) && value != string(swarm.PortConfigProtocolUDP) {
+				return fmt.Errorf("invalid protocol value %s", value)
+			}
+
+			pConfig.Protocol = swarm.PortConfigProtocol(value)
+		case portOptMode:
+			if value != string(swarm.PortConfigPublishModeIngress) && value != string(swarm.PortConfigPublishModeHost) {
+				return fmt.Errorf("invalid publish mode value %s", value)
+			}
+
+			pConfig.PublishMode = swarm.PortConfigPublishMode(value)
+		case portOptTargetPort:
+			tPort, err := strconv.ParseUint(value, 10, 16)
+			if err != nil {
+				return err
+			}
+
+			pConfig.TargetPort = uint32(tPort)
+		case portOptPublishedPort:
+			pPort, err := strconv.ParseUint(value, 10, 16)
+			if err != nil {
+				return err
+			}
+
+			pConfig.PublishedPort = uint32(pPort)
+		default:
+			return fmt.Errorf("invalid field key %s", key)
+		}
+	}
+
+	if pConfig.TargetPort == 0 {
+		return fmt.Errorf("missing mandatory field %q", portOptTargetPort)
+	}
+
+	p.ports = append(p.ports, pConfig)
+	return nil
+}
+
+// Type returns the type of this option
+func (p *PortOpt) Type() string {
+	return "port"
+}
+
+// String returns a string repr of this option
+func (p *PortOpt) String() string {
+	ports := []string{}
+	for _, port := range p.ports {
+		repr := fmt.Sprintf("%v:%v/%s/%s", port.PublishedPort, port.TargetPort, port.Protocol, port.PublishMode)
+		ports = append(ports, repr)
+	}
+	return strings.Join(ports, ", ")
+}
+
+// Value returns the ports
+func (p *PortOpt) Value() []swarm.PortConfig {
+	return p.ports
+}