From 14ac9f60d0174256e0713701ebffaf5ca827da71 Mon Sep 17 00:00:00 2001 From: Jana Radhakrishnan Date: Thu, 10 Nov 2016 12:13:26 -0800 Subject: [PATCH] Add support for host port PublishMode in services Add api/cli support for adding host port PublishMode in services. Signed-off-by: Jana Radhakrishnan --- api/types/swarm/network.go | 15 +++ api/types/swarm/task.go | 7 ++ cli/command/service/create.go | 4 +- cli/command/service/opts.go | 14 ++- cli/command/service/update.go | 55 ++++++++++- cli/command/service/update_test.go | 18 +--- cli/command/task/print.go | 20 +++- daemon/cluster/convert/network.go | 2 + daemon/cluster/convert/service.go | 1 + daemon/cluster/convert/task.go | 14 +++ .../cluster/executor/container/container.go | 77 ++++++++++++--- .../cluster/executor/container/controller.go | 74 ++++++++++++++ .../docker_cli_service_update_test.go | 1 + integration-cli/docker_cli_swarm_test.go | 5 +- opts/port.go | 99 +++++++++++++++++++ 15 files changed, 362 insertions(+), 44 deletions(-) create mode 100644 opts/port.go diff --git a/api/types/swarm/network.go b/api/types/swarm/network.go index 76b0bea1b5..2ba5339117 100644 --- a/api/types/swarm/network.go +++ b/api/types/swarm/network.go @@ -31,8 +31,23 @@ type PortConfig struct { TargetPort uint32 `json:",omitempty"` // PublishedPort is the port on the swarm hosts 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. type PortConfigProtocol string diff --git a/api/types/swarm/task.go b/api/types/swarm/task.go index bb28eec253..ace12cc89f 100644 --- a/api/types/swarm/task.go +++ b/api/types/swarm/task.go @@ -111,6 +111,7 @@ type TaskStatus struct { Message string `json:",omitempty"` Err string `json:",omitempty"` ContainerStatus ContainerStatus `json:",omitempty"` + PortStatus PortStatus `json:",omitempty"` } // ContainerStatus represents the status of a container. @@ -119,3 +120,9 @@ type ContainerStatus struct { PID 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"` +} diff --git a/cli/command/service/create.go b/cli/command/service/create.go index 17cf19625f..335867186a 100644 --- a/cli/command/service/create.go +++ b/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.networks, flagNetwork, "Network attachments") 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.dns, flagDNS, "Set custom DNS servers") flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") 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.endpoint.expandedPorts, flagPort, "Publish a port") flags.SetInterspersed(false) return cmd diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 4ea78c6af7..7da8338512 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -287,14 +287,15 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { } type endpointOptions struct { - mode string - ports opts.ListOpts + mode string + publishPorts opts.ListOpts + expandedPorts opts.PortOpt } func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { portConfigs := []swarm.PortConfig{} // 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 { portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...) @@ -302,7 +303,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { return &swarm.EndpointSpec{ 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), envFile: opts.NewListOpts(nil), endpoint: endpointOptions{ - ports: opts.NewListOpts(ValidatePort), + publishPorts: opts.NewListOpts(ValidatePort), }, groups: opts.NewListOpts(nil), logDriver: newLogDriverOptions(), @@ -647,6 +648,9 @@ const ( flagPublish = "publish" flagPublishRemove = "publish-rm" flagPublishAdd = "publish-add" + flagPort = "port" + flagPortAdd = "port-add" + flagPortRemove = "port-rm" flagReplicas = "replicas" flagReserveCPU = "reserve-cpu" flagReserveMemory = "reserve-memory" diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 1214b03a53..d2639a62db 100644 --- a/cli/command/service/update.go +++ b/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(), flagMountRemove, "Remove a mount by its target path") 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(), flagDNSRemove, "Remove a custom DNS server") 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.mounts, flagMountAdd, "Add or update a mount on a service") 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.dns, flagDNSAdd, "Add or update a custom DNS server") 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 { spec.EndpointSpec = &swarm.EndpointSpec{} } @@ -627,7 +631,13 @@ func portConfigToString(portConfig *swarm.PortConfig) string { if protocol == "" { 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 { @@ -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 for _, entry := range *portConfig { 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() + 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{} portLoop: for _, port := range portSet { @@ -666,14 +693,36 @@ 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) } + // Sort the PortConfig to avoid unnecessary updates sort.Sort(byPortConfig(newPorts)) *portConfig = newPorts 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 { return (string(port.Protocol) == targetPort.Proto() && port.TargetPort == uint32(targetPort.Int())) diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index a3736090ae..998d06d3bd 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -238,7 +238,7 @@ func TestUpdatePortsDuplicateEntries(t *testing.T) { func TestUpdatePortsDuplicateKeys(t *testing.T) { // Test case for #25375 flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "80:20") + flags.Set("publish-add", "80:80") portConfigs := []swarm.PortConfig{ {TargetPort: 80, PublishedPort: 80}, @@ -247,21 +247,7 @@ func TestUpdatePortsDuplicateKeys(t *testing.T) { err := updatePorts(flags, &portConfigs) assert.Equal(t, err, nil) 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) { diff --git a/cli/command/task/print.go b/cli/command/task/print.go index 45af178a42..2c5b2eecdd 100644 --- a/cli/command/task/print.go +++ b/cli/command/task/print.go @@ -17,10 +17,25 @@ import ( ) 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 ) +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 func (t tasksBySlot) Len() int { @@ -51,7 +66,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task // Ignore flushing errors 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 { return err @@ -113,6 +128,7 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr command.PrettyPrint(task.Status.State), strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))), taskErr, + portStatus(task.Status.PortStatus), ) } return nil diff --git a/daemon/cluster/convert/network.go b/daemon/cluster/convert/network.go index f643f620a6..1d9877f93c 100644 --- a/daemon/cluster/convert/network.go +++ b/daemon/cluster/convert/network.go @@ -93,6 +93,7 @@ func endpointSpecFromGRPC(es *swarmapi.EndpointSpec) *types.EndpointSpec { endpointSpec.Ports = append(endpointSpec.Ports, types.PortConfig{ Name: portState.Name, 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, PublishedPort: portState.PublishedPort, }) @@ -112,6 +113,7 @@ func endpointFromGRPC(e *swarmapi.Endpoint) types.Endpoint { endpoint.Ports = append(endpoint.Ports, types.PortConfig{ Name: portState.Name, 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, PublishedPort: portState.PublishedPort, }) diff --git a/daemon/cluster/convert/service.go b/daemon/cluster/convert/service.go index 35718ee583..aa68e01f44 100644 --- a/daemon/cluster/convert/service.go +++ b/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{ Name: portConfig.Name, 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, PublishedPort: portConfig.PublishedPort, }) diff --git a/daemon/cluster/convert/task.go b/daemon/cluster/convert/task.go index 5f3c3471c9..d0cf89c288 100644 --- a/daemon/cluster/convert/task.go +++ b/daemon/cluster/convert/task.go @@ -63,5 +63,19 @@ func TaskFromGRPC(t swarmapi.Task) types.Task { 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 } diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 6cc21ab3bb..bed613ca8e 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net" + "strconv" "strings" "time" @@ -17,6 +18,7 @@ import ( volumetypes "github.com/docker/docker/api/types/volume" clustertypes "github.com/docker/docker/daemon/cluster/provider" "github.com/docker/docker/reference" + "github.com/docker/go-connections/nat" "github.com/docker/swarmkit/agent/exec" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/protobuf/ptypes" @@ -136,17 +138,61 @@ func (c *containerConfig) image() 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 { 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 { @@ -333,10 +379,11 @@ func getMountMask(m *api.Mount) string { func (c *containerConfig) hostConfig() *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 { @@ -525,6 +572,10 @@ func (c *containerConfig) serviceConfig() *clustertypes.ServiceConfig { if c.task.Endpoint != nil { for _, ePort := range c.task.Endpoint.Ports { + if ePort.PublishMode != api.PublishModeIngress { + continue + } + svcCfg.ExposedPorts = append(svcCfg.ExposedPorts, &clustertypes.PortConfig{ Name: ePort.Name, Protocol: int32(ePort.Protocol), diff --git a/daemon/cluster/executor/container/controller.go b/daemon/cluster/executor/container/controller.go index 103f47e965..75f286a217 100644 --- a/daemon/cluster/executor/container/controller.go +++ b/daemon/cluster/executor/container/controller.go @@ -7,11 +7,14 @@ import ( "fmt" "io" "os" + "strconv" + "strings" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" executorpkg "github.com/docker/docker/daemon/cluster/executor" + "github.com/docker/go-connections/nat" "github.com/docker/libnetwork" "github.com/docker/swarmkit/agent/exec" "github.com/docker/swarmkit/api" @@ -69,6 +72,19 @@ func (r *controller) ContainerStatus(ctx context.Context) (*api.ContainerStatus, 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. func (r *controller) Update(ctx context.Context, t *api.Task) error { // 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 } +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 { code int cause error diff --git a/integration-cli/docker_cli_service_update_test.go b/integration-cli/docker_cli_service_update_test.go index 548f8e64c2..6400f6d606 100644 --- a/integration-cli/docker_cli_service_update_test.go +++ b/integration-cli/docker_cli_service_update_test.go @@ -32,6 +32,7 @@ func (s *DockerSwarmSuite) TestServiceUpdatePort(c *check.C) { Protocol: "tcp", PublishedPort: 8082, TargetPort: 8083, + PublishMode: "ingress", }, } diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index 1c5d059094..cffab330ed 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/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) 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) 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) { diff --git a/opts/port.go b/opts/port.go new file mode 100644 index 0000000000..ef3f12a465 --- /dev/null +++ b/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 +}