Add api/cli support for adding host port PublishMode in services.
Signed-off-by: Jana Radhakrishnan <mrjana@docker.com>
| ... | ... |
@@ -31,8 +31,23 @@ type PortConfig struct {
|
| 31 | 31 |
TargetPort uint32 `json:",omitempty"` |
| 32 | 32 |
// PublishedPort is the port on the swarm hosts |
| 33 | 33 |
PublishedPort uint32 `json:",omitempty"` |
| 34 |
+ // PublishMode is the mode in which port is published |
|
| 35 |
+ PublishMode PortConfigPublishMode `json:",omitempty"` |
|
| 34 | 36 |
} |
| 35 | 37 |
|
| 38 |
+// PortConfigPublishMode represents the mode in which the port is to |
|
| 39 |
+// be published. |
|
| 40 |
+type PortConfigPublishMode string |
|
| 41 |
+ |
|
| 42 |
+const ( |
|
| 43 |
+ // PortConfigPublishModeIngress is used for ports published |
|
| 44 |
+ // for ingress load balancing using routing mesh. |
|
| 45 |
+ PortConfigPublishModeIngress PortConfigPublishMode = "ingress" |
|
| 46 |
+ // PortConfigPublishModeHost is used for ports published |
|
| 47 |
+ // for direct host level access on the host where the task is running. |
|
| 48 |
+ PortConfigPublishModeHost PortConfigPublishMode = "host" |
|
| 49 |
+) |
|
| 50 |
+ |
|
| 36 | 51 |
// PortConfigProtocol represents the protocol of a port. |
| 37 | 52 |
type PortConfigProtocol string |
| 38 | 53 |
|
| ... | ... |
@@ -111,6 +111,7 @@ type TaskStatus struct {
|
| 111 | 111 |
Message string `json:",omitempty"` |
| 112 | 112 |
Err string `json:",omitempty"` |
| 113 | 113 |
ContainerStatus ContainerStatus `json:",omitempty"` |
| 114 |
+ PortStatus PortStatus `json:",omitempty"` |
|
| 114 | 115 |
} |
| 115 | 116 |
|
| 116 | 117 |
// ContainerStatus represents the status of a container. |
| ... | ... |
@@ -119,3 +120,9 @@ type ContainerStatus struct {
|
| 119 | 119 |
PID int `json:",omitempty"` |
| 120 | 120 |
ExitCode int `json:",omitempty"` |
| 121 | 121 |
} |
| 122 |
+ |
|
| 123 |
+// PortStatus represents the port status of a task's host ports whose |
|
| 124 |
+// service has published host ports |
|
| 125 |
+type PortStatus struct {
|
|
| 126 |
+ Ports []PortConfig `json:",omitempty"` |
|
| 127 |
+} |
| ... | ... |
@@ -40,12 +40,14 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 40 | 40 |
flags.Var(&opts.constraints, flagConstraint, "Placement constraints") |
| 41 | 41 |
flags.Var(&opts.networks, flagNetwork, "Network attachments") |
| 42 | 42 |
flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") |
| 43 |
- flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") |
|
| 43 |
+ flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port") |
|
| 44 |
+ flags.MarkHidden(flagPublish) |
|
| 44 | 45 |
flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") |
| 45 | 46 |
flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") |
| 46 | 47 |
flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") |
| 47 | 48 |
flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") |
| 48 | 49 |
flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") |
| 50 |
+ flags.Var(&opts.endpoint.expandedPorts, flagPort, "Publish a port") |
|
| 49 | 51 |
|
| 50 | 52 |
flags.SetInterspersed(false) |
| 51 | 53 |
return cmd |
| ... | ... |
@@ -287,14 +287,15 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
|
| 287 | 287 |
} |
| 288 | 288 |
|
| 289 | 289 |
type endpointOptions struct {
|
| 290 |
- mode string |
|
| 291 |
- ports opts.ListOpts |
|
| 290 |
+ mode string |
|
| 291 |
+ publishPorts opts.ListOpts |
|
| 292 |
+ expandedPorts opts.PortOpt |
|
| 292 | 293 |
} |
| 293 | 294 |
|
| 294 | 295 |
func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
|
| 295 | 296 |
portConfigs := []swarm.PortConfig{}
|
| 296 | 297 |
// We can ignore errors because the format was already validated by ValidatePort |
| 297 |
- ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll()) |
|
| 298 |
+ ports, portBindings, _ := nat.ParsePortSpecs(e.publishPorts.GetAll()) |
|
| 298 | 299 |
|
| 299 | 300 |
for port := range ports {
|
| 300 | 301 |
portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...) |
| ... | ... |
@@ -302,7 +303,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
|
| 302 | 302 |
|
| 303 | 303 |
return &swarm.EndpointSpec{
|
| 304 | 304 |
Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), |
| 305 |
- Ports: portConfigs, |
|
| 305 |
+ Ports: append(portConfigs, e.expandedPorts.Value()...), |
|
| 306 | 306 |
} |
| 307 | 307 |
} |
| 308 | 308 |
|
| ... | ... |
@@ -459,7 +460,7 @@ func newServiceOptions() *serviceOptions {
|
| 459 | 459 |
env: opts.NewListOpts(runconfigopts.ValidateEnv), |
| 460 | 460 |
envFile: opts.NewListOpts(nil), |
| 461 | 461 |
endpoint: endpointOptions{
|
| 462 |
- ports: opts.NewListOpts(ValidatePort), |
|
| 462 |
+ publishPorts: opts.NewListOpts(ValidatePort), |
|
| 463 | 463 |
}, |
| 464 | 464 |
groups: opts.NewListOpts(nil), |
| 465 | 465 |
logDriver: newLogDriverOptions(), |
| ... | ... |
@@ -647,6 +648,9 @@ const ( |
| 647 | 647 |
flagPublish = "publish" |
| 648 | 648 |
flagPublishRemove = "publish-rm" |
| 649 | 649 |
flagPublishAdd = "publish-add" |
| 650 |
+ flagPort = "port" |
|
| 651 |
+ flagPortAdd = "port-add" |
|
| 652 |
+ flagPortRemove = "port-rm" |
|
| 650 | 653 |
flagReplicas = "replicas" |
| 651 | 654 |
flagReserveCPU = "reserve-cpu" |
| 652 | 655 |
flagReserveMemory = "reserve-memory" |
| ... | ... |
@@ -48,6 +48,8 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 48 | 48 |
flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") |
| 49 | 49 |
flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") |
| 50 | 50 |
flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port") |
| 51 |
+ flags.MarkHidden(flagPublishRemove) |
|
| 52 |
+ flags.Var(newListOptsVar(), flagPortRemove, "Remove a port(target-port mandatory)") |
|
| 51 | 53 |
flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") |
| 52 | 54 |
flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") |
| 53 | 55 |
flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") |
| ... | ... |
@@ -60,7 +62,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 60 | 60 |
flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service") |
| 61 | 61 |
flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") |
| 62 | 62 |
flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") |
| 63 |
- flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") |
|
| 63 |
+ flags.Var(&opts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") |
|
| 64 |
+ flags.MarkHidden(flagPublishAdd) |
|
| 65 |
+ flags.Var(&opts.endpoint.expandedPorts, flagPortAdd, "Add or update a port") |
|
| 64 | 66 |
flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") |
| 65 | 67 |
flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server") |
| 66 | 68 |
flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") |
| ... | ... |
@@ -267,7 +271,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
|
| 267 | 267 |
} |
| 268 | 268 |
} |
| 269 | 269 |
|
| 270 |
- if anyChanged(flags, flagPublishAdd, flagPublishRemove) {
|
|
| 270 |
+ if anyChanged(flags, flagPublishAdd, flagPublishRemove, flagPortAdd, flagPortRemove) {
|
|
| 271 | 271 |
if spec.EndpointSpec == nil {
|
| 272 | 272 |
spec.EndpointSpec = &swarm.EndpointSpec{}
|
| 273 | 273 |
} |
| ... | ... |
@@ -627,7 +631,13 @@ func portConfigToString(portConfig *swarm.PortConfig) string {
|
| 627 | 627 |
if protocol == "" {
|
| 628 | 628 |
protocol = "tcp" |
| 629 | 629 |
} |
| 630 |
- return fmt.Sprintf("%v/%s", portConfig.PublishedPort, protocol)
|
|
| 630 |
+ |
|
| 631 |
+ mode := portConfig.PublishMode |
|
| 632 |
+ if mode == "" {
|
|
| 633 |
+ mode = "ingress" |
|
| 634 |
+ } |
|
| 635 |
+ |
|
| 636 |
+ return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode)
|
|
| 631 | 637 |
} |
| 632 | 638 |
|
| 633 | 639 |
func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
|
| ... | ... |
@@ -649,6 +659,15 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
|
| 649 | 649 |
} |
| 650 | 650 |
} |
| 651 | 651 |
|
| 652 |
+ if flags.Changed(flagPortAdd) {
|
|
| 653 |
+ for _, entry := range flags.Lookup(flagPortAdd).Value.(*opts.PortOpt).Value() {
|
|
| 654 |
+ if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry {
|
|
| 655 |
+ 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)
|
|
| 656 |
+ } |
|
| 657 |
+ portSet[portConfigToString(&entry)] = entry |
|
| 658 |
+ } |
|
| 659 |
+ } |
|
| 660 |
+ |
|
| 652 | 661 |
// Override previous PortConfig in service if there is any duplicate |
| 653 | 662 |
for _, entry := range *portConfig {
|
| 654 | 663 |
if _, ok := portSet[portConfigToString(&entry)]; !ok {
|
| ... | ... |
@@ -657,6 +676,14 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
|
| 657 | 657 |
} |
| 658 | 658 |
|
| 659 | 659 |
toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll() |
| 660 |
+ removePortCSV := flags.Lookup(flagPortRemove).Value.(*opts.ListOpts).GetAll() |
|
| 661 |
+ removePortOpts := &opts.PortOpt{}
|
|
| 662 |
+ for _, portCSV := range removePortCSV {
|
|
| 663 |
+ if err := removePortOpts.Set(portCSV); err != nil {
|
|
| 664 |
+ return err |
|
| 665 |
+ } |
|
| 666 |
+ } |
|
| 667 |
+ |
|
| 660 | 668 |
newPorts := []swarm.PortConfig{}
|
| 661 | 669 |
portLoop: |
| 662 | 670 |
for _, port := range portSet {
|
| ... | ... |
@@ -666,14 +693,36 @@ portLoop: |
| 666 | 666 |
continue portLoop |
| 667 | 667 |
} |
| 668 | 668 |
} |
| 669 |
+ |
|
| 670 |
+ for _, pConfig := range removePortOpts.Value() {
|
|
| 671 |
+ if equalProtocol(port.Protocol, pConfig.Protocol) && |
|
| 672 |
+ port.TargetPort == pConfig.TargetPort && |
|
| 673 |
+ equalPublishMode(port.PublishMode, pConfig.PublishMode) {
|
|
| 674 |
+ continue portLoop |
|
| 675 |
+ } |
|
| 676 |
+ } |
|
| 677 |
+ |
|
| 669 | 678 |
newPorts = append(newPorts, port) |
| 670 | 679 |
} |
| 680 |
+ |
|
| 671 | 681 |
// Sort the PortConfig to avoid unnecessary updates |
| 672 | 682 |
sort.Sort(byPortConfig(newPorts)) |
| 673 | 683 |
*portConfig = newPorts |
| 674 | 684 |
return nil |
| 675 | 685 |
} |
| 676 | 686 |
|
| 687 |
+func equalProtocol(prot1, prot2 swarm.PortConfigProtocol) bool {
|
|
| 688 |
+ return prot1 == prot2 || |
|
| 689 |
+ (prot1 == swarm.PortConfigProtocol("") && prot2 == swarm.PortConfigProtocolTCP) ||
|
|
| 690 |
+ (prot2 == swarm.PortConfigProtocol("") && prot1 == swarm.PortConfigProtocolTCP)
|
|
| 691 |
+} |
|
| 692 |
+ |
|
| 693 |
+func equalPublishMode(mode1, mode2 swarm.PortConfigPublishMode) bool {
|
|
| 694 |
+ return mode1 == mode2 || |
|
| 695 |
+ (mode1 == swarm.PortConfigPublishMode("") && mode2 == swarm.PortConfigPublishModeIngress) ||
|
|
| 696 |
+ (mode2 == swarm.PortConfigPublishMode("") && mode1 == swarm.PortConfigPublishModeIngress)
|
|
| 697 |
+} |
|
| 698 |
+ |
|
| 677 | 699 |
func equalPort(targetPort nat.Port, port swarm.PortConfig) bool {
|
| 678 | 700 |
return (string(port.Protocol) == targetPort.Proto() && |
| 679 | 701 |
port.TargetPort == uint32(targetPort.Int())) |
| ... | ... |
@@ -238,7 +238,7 @@ func TestUpdatePortsDuplicateEntries(t *testing.T) {
|
| 238 | 238 |
func TestUpdatePortsDuplicateKeys(t *testing.T) {
|
| 239 | 239 |
// Test case for #25375 |
| 240 | 240 |
flags := newUpdateCommand(nil).Flags() |
| 241 |
- flags.Set("publish-add", "80:20")
|
|
| 241 |
+ flags.Set("publish-add", "80:80")
|
|
| 242 | 242 |
|
| 243 | 243 |
portConfigs := []swarm.PortConfig{
|
| 244 | 244 |
{TargetPort: 80, PublishedPort: 80},
|
| ... | ... |
@@ -247,21 +247,7 @@ func TestUpdatePortsDuplicateKeys(t *testing.T) {
|
| 247 | 247 |
err := updatePorts(flags, &portConfigs) |
| 248 | 248 |
assert.Equal(t, err, nil) |
| 249 | 249 |
assert.Equal(t, len(portConfigs), 1) |
| 250 |
- assert.Equal(t, portConfigs[0].TargetPort, uint32(20)) |
|
| 251 |
-} |
|
| 252 |
- |
|
| 253 |
-func TestUpdatePortsConflictingFlags(t *testing.T) {
|
|
| 254 |
- // Test case for #25375 |
|
| 255 |
- flags := newUpdateCommand(nil).Flags() |
|
| 256 |
- flags.Set("publish-add", "80:80")
|
|
| 257 |
- flags.Set("publish-add", "80:20")
|
|
| 258 |
- |
|
| 259 |
- portConfigs := []swarm.PortConfig{
|
|
| 260 |
- {TargetPort: 80, PublishedPort: 80},
|
|
| 261 |
- } |
|
| 262 |
- |
|
| 263 |
- err := updatePorts(flags, &portConfigs) |
|
| 264 |
- assert.Error(t, err, "conflicting port mapping") |
|
| 250 |
+ assert.Equal(t, portConfigs[0].TargetPort, uint32(80)) |
|
| 265 | 251 |
} |
| 266 | 252 |
|
| 267 | 253 |
func TestUpdateHealthcheckTable(t *testing.T) {
|
| ... | ... |
@@ -17,10 +17,25 @@ import ( |
| 17 | 17 |
) |
| 18 | 18 |
|
| 19 | 19 |
const ( |
| 20 |
- psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\n" |
|
| 20 |
+ psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n" |
|
| 21 | 21 |
maxErrLength = 30 |
| 22 | 22 |
) |
| 23 | 23 |
|
| 24 |
+type portStatus swarm.PortStatus |
|
| 25 |
+ |
|
| 26 |
+func (ps portStatus) String() string {
|
|
| 27 |
+ if len(ps.Ports) == 0 {
|
|
| 28 |
+ return "" |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ str := fmt.Sprintf("*:%d->%d/%s", ps.Ports[0].PublishedPort, ps.Ports[0].TargetPort, ps.Ports[0].Protocol)
|
|
| 32 |
+ for _, pConfig := range ps.Ports[1:] {
|
|
| 33 |
+ str += fmt.Sprintf(",*:%d->%d/%s", pConfig.PublishedPort, pConfig.TargetPort, pConfig.Protocol)
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ return str |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 24 | 39 |
type tasksBySlot []swarm.Task |
| 25 | 40 |
|
| 26 | 41 |
func (t tasksBySlot) Len() int {
|
| ... | ... |
@@ -51,7 +66,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task |
| 51 | 51 |
|
| 52 | 52 |
// Ignore flushing errors |
| 53 | 53 |
defer writer.Flush() |
| 54 |
- fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR"}, "\t"))
|
|
| 54 |
+ fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t"))
|
|
| 55 | 55 |
|
| 56 | 56 |
if err := print(writer, ctx, tasks, resolver, noTrunc); err != nil {
|
| 57 | 57 |
return err |
| ... | ... |
@@ -113,6 +128,7 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr |
| 113 | 113 |
command.PrettyPrint(task.Status.State), |
| 114 | 114 |
strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))), |
| 115 | 115 |
taskErr, |
| 116 |
+ portStatus(task.Status.PortStatus), |
|
| 116 | 117 |
) |
| 117 | 118 |
} |
| 118 | 119 |
return nil |
| ... | ... |
@@ -93,6 +93,7 @@ func endpointSpecFromGRPC(es *swarmapi.EndpointSpec) *types.EndpointSpec {
|
| 93 | 93 |
endpointSpec.Ports = append(endpointSpec.Ports, types.PortConfig{
|
| 94 | 94 |
Name: portState.Name, |
| 95 | 95 |
Protocol: types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])), |
| 96 |
+ PublishMode: types.PortConfigPublishMode(strings.ToLower(swarmapi.PortConfig_PublishMode_name[int32(portState.PublishMode)])), |
|
| 96 | 97 |
TargetPort: portState.TargetPort, |
| 97 | 98 |
PublishedPort: portState.PublishedPort, |
| 98 | 99 |
}) |
| ... | ... |
@@ -112,6 +113,7 @@ func endpointFromGRPC(e *swarmapi.Endpoint) types.Endpoint {
|
| 112 | 112 |
endpoint.Ports = append(endpoint.Ports, types.PortConfig{
|
| 113 | 113 |
Name: portState.Name, |
| 114 | 114 |
Protocol: types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])), |
| 115 |
+ PublishMode: types.PortConfigPublishMode(strings.ToLower(swarmapi.PortConfig_PublishMode_name[int32(portState.PublishMode)])), |
|
| 115 | 116 |
TargetPort: portState.TargetPort, |
| 116 | 117 |
PublishedPort: portState.PublishedPort, |
| 117 | 118 |
}) |
| ... | ... |
@@ -199,6 +199,7 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
|
| 199 | 199 |
spec.Endpoint.Ports = append(spec.Endpoint.Ports, &swarmapi.PortConfig{
|
| 200 | 200 |
Name: portConfig.Name, |
| 201 | 201 |
Protocol: swarmapi.PortConfig_Protocol(swarmapi.PortConfig_Protocol_value[strings.ToUpper(string(portConfig.Protocol))]), |
| 202 |
+ PublishMode: swarmapi.PortConfig_PublishMode(swarmapi.PortConfig_PublishMode_value[strings.ToUpper(string(portConfig.PublishMode))]), |
|
| 202 | 203 |
TargetPort: portConfig.TargetPort, |
| 203 | 204 |
PublishedPort: portConfig.PublishedPort, |
| 204 | 205 |
}) |
| ... | ... |
@@ -63,5 +63,19 @@ func TaskFromGRPC(t swarmapi.Task) types.Task {
|
| 63 | 63 |
task.NetworksAttachments = append(task.NetworksAttachments, networkAttachementFromGRPC(na)) |
| 64 | 64 |
} |
| 65 | 65 |
|
| 66 |
+ if t.Status.PortStatus == nil {
|
|
| 67 |
+ return task |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ for _, p := range t.Status.PortStatus.Ports {
|
|
| 71 |
+ task.Status.PortStatus.Ports = append(task.Status.PortStatus.Ports, types.PortConfig{
|
|
| 72 |
+ Name: p.Name, |
|
| 73 |
+ Protocol: types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(p.Protocol)])), |
|
| 74 |
+ PublishMode: types.PortConfigPublishMode(strings.ToLower(swarmapi.PortConfig_PublishMode_name[int32(p.PublishMode)])), |
|
| 75 |
+ TargetPort: p.TargetPort, |
|
| 76 |
+ PublishedPort: p.PublishedPort, |
|
| 77 |
+ }) |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 66 | 80 |
return task |
| 67 | 81 |
} |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"errors" |
| 5 | 5 |
"fmt" |
| 6 | 6 |
"net" |
| 7 |
+ "strconv" |
|
| 7 | 8 |
"strings" |
| 8 | 9 |
"time" |
| 9 | 10 |
|
| ... | ... |
@@ -17,6 +18,7 @@ import ( |
| 17 | 17 |
volumetypes "github.com/docker/docker/api/types/volume" |
| 18 | 18 |
clustertypes "github.com/docker/docker/daemon/cluster/provider" |
| 19 | 19 |
"github.com/docker/docker/reference" |
| 20 |
+ "github.com/docker/go-connections/nat" |
|
| 20 | 21 |
"github.com/docker/swarmkit/agent/exec" |
| 21 | 22 |
"github.com/docker/swarmkit/api" |
| 22 | 23 |
"github.com/docker/swarmkit/protobuf/ptypes" |
| ... | ... |
@@ -136,17 +138,61 @@ func (c *containerConfig) image() string {
|
| 136 | 136 |
return reference.WithDefaultTag(ref).String() |
| 137 | 137 |
} |
| 138 | 138 |
|
| 139 |
+func (c *containerConfig) portBindings() nat.PortMap {
|
|
| 140 |
+ portBindings := nat.PortMap{}
|
|
| 141 |
+ if c.task.Endpoint == nil {
|
|
| 142 |
+ return portBindings |
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ for _, portConfig := range c.task.Endpoint.Ports {
|
|
| 146 |
+ if portConfig.PublishMode != api.PublishModeHost {
|
|
| 147 |
+ continue |
|
| 148 |
+ } |
|
| 149 |
+ |
|
| 150 |
+ port := nat.Port(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String())))
|
|
| 151 |
+ binding := []nat.PortBinding{
|
|
| 152 |
+ {},
|
|
| 153 |
+ } |
|
| 154 |
+ |
|
| 155 |
+ if portConfig.PublishedPort != 0 {
|
|
| 156 |
+ binding[0].HostPort = strconv.Itoa(int(portConfig.PublishedPort)) |
|
| 157 |
+ } |
|
| 158 |
+ portBindings[port] = binding |
|
| 159 |
+ } |
|
| 160 |
+ |
|
| 161 |
+ return portBindings |
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+func (c *containerConfig) exposedPorts() map[nat.Port]struct{} {
|
|
| 165 |
+ exposedPorts := make(map[nat.Port]struct{})
|
|
| 166 |
+ if c.task.Endpoint == nil {
|
|
| 167 |
+ return exposedPorts |
|
| 168 |
+ } |
|
| 169 |
+ |
|
| 170 |
+ for _, portConfig := range c.task.Endpoint.Ports {
|
|
| 171 |
+ if portConfig.PublishMode != api.PublishModeHost {
|
|
| 172 |
+ continue |
|
| 173 |
+ } |
|
| 174 |
+ |
|
| 175 |
+ port := nat.Port(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String())))
|
|
| 176 |
+ exposedPorts[port] = struct{}{}
|
|
| 177 |
+ } |
|
| 178 |
+ |
|
| 179 |
+ return exposedPorts |
|
| 180 |
+} |
|
| 181 |
+ |
|
| 139 | 182 |
func (c *containerConfig) config() *enginecontainer.Config {
|
| 140 | 183 |
config := &enginecontainer.Config{
|
| 141 |
- Labels: c.labels(), |
|
| 142 |
- Tty: c.spec().TTY, |
|
| 143 |
- User: c.spec().User, |
|
| 144 |
- Env: c.spec().Env, |
|
| 145 |
- Hostname: c.spec().Hostname, |
|
| 146 |
- WorkingDir: c.spec().Dir, |
|
| 147 |
- Image: c.image(), |
|
| 148 |
- Volumes: c.volumes(), |
|
| 149 |
- Healthcheck: c.healthcheck(), |
|
| 184 |
+ Labels: c.labels(), |
|
| 185 |
+ Tty: c.spec().TTY, |
|
| 186 |
+ User: c.spec().User, |
|
| 187 |
+ Env: c.spec().Env, |
|
| 188 |
+ Hostname: c.spec().Hostname, |
|
| 189 |
+ WorkingDir: c.spec().Dir, |
|
| 190 |
+ Image: c.image(), |
|
| 191 |
+ Volumes: c.volumes(), |
|
| 192 |
+ ExposedPorts: c.exposedPorts(), |
|
| 193 |
+ Healthcheck: c.healthcheck(), |
|
| 150 | 194 |
} |
| 151 | 195 |
|
| 152 | 196 |
if len(c.spec().Command) > 0 {
|
| ... | ... |
@@ -333,10 +379,11 @@ func getMountMask(m *api.Mount) string {
|
| 333 | 333 |
|
| 334 | 334 |
func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
|
| 335 | 335 |
hc := &enginecontainer.HostConfig{
|
| 336 |
- Resources: c.resources(), |
|
| 337 |
- Binds: c.binds(), |
|
| 338 |
- Tmpfs: c.tmpfs(), |
|
| 339 |
- GroupAdd: c.spec().Groups, |
|
| 336 |
+ Resources: c.resources(), |
|
| 337 |
+ Binds: c.binds(), |
|
| 338 |
+ Tmpfs: c.tmpfs(), |
|
| 339 |
+ GroupAdd: c.spec().Groups, |
|
| 340 |
+ PortBindings: c.portBindings(), |
|
| 340 | 341 |
} |
| 341 | 342 |
|
| 342 | 343 |
if c.spec().DNSConfig != nil {
|
| ... | ... |
@@ -525,6 +572,10 @@ func (c *containerConfig) serviceConfig() *clustertypes.ServiceConfig {
|
| 525 | 525 |
|
| 526 | 526 |
if c.task.Endpoint != nil {
|
| 527 | 527 |
for _, ePort := range c.task.Endpoint.Ports {
|
| 528 |
+ if ePort.PublishMode != api.PublishModeIngress {
|
|
| 529 |
+ continue |
|
| 530 |
+ } |
|
| 531 |
+ |
|
| 528 | 532 |
svcCfg.ExposedPorts = append(svcCfg.ExposedPorts, &clustertypes.PortConfig{
|
| 529 | 533 |
Name: ePort.Name, |
| 530 | 534 |
Protocol: int32(ePort.Protocol), |
| ... | ... |
@@ -7,11 +7,14 @@ import ( |
| 7 | 7 |
"fmt" |
| 8 | 8 |
"io" |
| 9 | 9 |
"os" |
| 10 |
+ "strconv" |
|
| 11 |
+ "strings" |
|
| 10 | 12 |
"time" |
| 11 | 13 |
|
| 12 | 14 |
"github.com/docker/docker/api/types" |
| 13 | 15 |
"github.com/docker/docker/api/types/events" |
| 14 | 16 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 17 |
+ "github.com/docker/go-connections/nat" |
|
| 15 | 18 |
"github.com/docker/libnetwork" |
| 16 | 19 |
"github.com/docker/swarmkit/agent/exec" |
| 17 | 20 |
"github.com/docker/swarmkit/api" |
| ... | ... |
@@ -69,6 +72,19 @@ func (r *controller) ContainerStatus(ctx context.Context) (*api.ContainerStatus, |
| 69 | 69 |
return parseContainerStatus(ctnr) |
| 70 | 70 |
} |
| 71 | 71 |
|
| 72 |
+func (r *controller) PortStatus(ctx context.Context) (*api.PortStatus, error) {
|
|
| 73 |
+ ctnr, err := r.adapter.inspect(ctx) |
|
| 74 |
+ if err != nil {
|
|
| 75 |
+ if isUnknownContainer(err) {
|
|
| 76 |
+ return nil, nil |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ return nil, err |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ return parsePortStatus(ctnr) |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 72 | 85 |
// Update tasks a recent task update and applies it to the container. |
| 73 | 86 |
func (r *controller) Update(ctx context.Context, t *api.Task) error {
|
| 74 | 87 |
// TODO(stevvooe): While assignment of tasks is idempotent, we do allow |
| ... | ... |
@@ -553,6 +569,64 @@ func parseContainerStatus(ctnr types.ContainerJSON) (*api.ContainerStatus, error |
| 553 | 553 |
return status, nil |
| 554 | 554 |
} |
| 555 | 555 |
|
| 556 |
+func parsePortStatus(ctnr types.ContainerJSON) (*api.PortStatus, error) {
|
|
| 557 |
+ status := &api.PortStatus{}
|
|
| 558 |
+ |
|
| 559 |
+ if ctnr.NetworkSettings != nil && len(ctnr.NetworkSettings.Ports) > 0 {
|
|
| 560 |
+ exposedPorts, err := parsePortMap(ctnr.NetworkSettings.Ports) |
|
| 561 |
+ if err != nil {
|
|
| 562 |
+ return nil, err |
|
| 563 |
+ } |
|
| 564 |
+ status.Ports = exposedPorts |
|
| 565 |
+ } |
|
| 566 |
+ |
|
| 567 |
+ return status, nil |
|
| 568 |
+} |
|
| 569 |
+ |
|
| 570 |
+func parsePortMap(portMap nat.PortMap) ([]*api.PortConfig, error) {
|
|
| 571 |
+ exposedPorts := make([]*api.PortConfig, 0, len(portMap)) |
|
| 572 |
+ |
|
| 573 |
+ for portProtocol, mapping := range portMap {
|
|
| 574 |
+ parts := strings.SplitN(string(portProtocol), "/", 2) |
|
| 575 |
+ if len(parts) != 2 {
|
|
| 576 |
+ return nil, fmt.Errorf("invalid port mapping: %s", portProtocol)
|
|
| 577 |
+ } |
|
| 578 |
+ |
|
| 579 |
+ port, err := strconv.ParseUint(parts[0], 10, 16) |
|
| 580 |
+ if err != nil {
|
|
| 581 |
+ return nil, err |
|
| 582 |
+ } |
|
| 583 |
+ |
|
| 584 |
+ protocol := api.ProtocolTCP |
|
| 585 |
+ switch strings.ToLower(parts[1]) {
|
|
| 586 |
+ case "tcp": |
|
| 587 |
+ protocol = api.ProtocolTCP |
|
| 588 |
+ case "udp": |
|
| 589 |
+ protocol = api.ProtocolUDP |
|
| 590 |
+ default: |
|
| 591 |
+ return nil, fmt.Errorf("invalid protocol: %s", parts[1])
|
|
| 592 |
+ } |
|
| 593 |
+ |
|
| 594 |
+ for _, binding := range mapping {
|
|
| 595 |
+ hostPort, err := strconv.ParseUint(binding.HostPort, 10, 16) |
|
| 596 |
+ if err != nil {
|
|
| 597 |
+ return nil, err |
|
| 598 |
+ } |
|
| 599 |
+ |
|
| 600 |
+ // TODO(aluzzardi): We're losing the port `name` here since |
|
| 601 |
+ // there's no way to retrieve it back from the Engine. |
|
| 602 |
+ exposedPorts = append(exposedPorts, &api.PortConfig{
|
|
| 603 |
+ PublishMode: api.PublishModeHost, |
|
| 604 |
+ Protocol: protocol, |
|
| 605 |
+ TargetPort: uint32(port), |
|
| 606 |
+ PublishedPort: uint32(hostPort), |
|
| 607 |
+ }) |
|
| 608 |
+ } |
|
| 609 |
+ } |
|
| 610 |
+ |
|
| 611 |
+ return exposedPorts, nil |
|
| 612 |
+} |
|
| 613 |
+ |
|
| 556 | 614 |
type exitError struct {
|
| 557 | 615 |
code int |
| 558 | 616 |
cause error |
| ... | ... |
@@ -321,12 +321,9 @@ func (s *DockerSwarmSuite) TestSwarmPublishAdd(c *check.C) {
|
| 321 | 321 |
out, err = d.cmdRetryOutOfSequence("service", "update", "--publish-add", "80:80", "--publish-add", "80:20", name)
|
| 322 | 322 |
c.Assert(err, checker.NotNil) |
| 323 | 323 |
|
| 324 |
- out, err = d.cmdRetryOutOfSequence("service", "update", "--publish-add", "80:20", name)
|
|
| 325 |
- c.Assert(err, checker.IsNil) |
|
| 326 |
- |
|
| 327 | 324 |
out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", name)
|
| 328 | 325 |
c.Assert(err, checker.IsNil) |
| 329 |
- c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 20 80}]")
|
|
| 326 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 80 80 ingress}]")
|
|
| 330 | 327 |
} |
| 331 | 328 |
|
| 332 | 329 |
func (s *DockerSwarmSuite) TestSwarmServiceWithGroup(c *check.C) {
|
| 333 | 330 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,99 @@ |
| 0 |
+package opts |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/csv" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+const ( |
|
| 12 |
+ portOptTargetPort = "target" |
|
| 13 |
+ portOptPublishedPort = "published" |
|
| 14 |
+ portOptProtocol = "protocol" |
|
| 15 |
+ portOptMode = "mode" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+type PortOpt struct {
|
|
| 19 |
+ ports []swarm.PortConfig |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+// Set a new port value |
|
| 23 |
+func (p *PortOpt) Set(value string) error {
|
|
| 24 |
+ csvReader := csv.NewReader(strings.NewReader(value)) |
|
| 25 |
+ fields, err := csvReader.Read() |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return err |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ pConfig := swarm.PortConfig{}
|
|
| 31 |
+ for _, field := range fields {
|
|
| 32 |
+ parts := strings.SplitN(field, "=", 2) |
|
| 33 |
+ if len(parts) != 2 {
|
|
| 34 |
+ return fmt.Errorf("invalid field %s", field)
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ key := strings.ToLower(parts[0]) |
|
| 38 |
+ value := strings.ToLower(parts[1]) |
|
| 39 |
+ |
|
| 40 |
+ switch key {
|
|
| 41 |
+ case portOptProtocol: |
|
| 42 |
+ if value != string(swarm.PortConfigProtocolTCP) && value != string(swarm.PortConfigProtocolUDP) {
|
|
| 43 |
+ return fmt.Errorf("invalid protocol value %s", value)
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ pConfig.Protocol = swarm.PortConfigProtocol(value) |
|
| 47 |
+ case portOptMode: |
|
| 48 |
+ if value != string(swarm.PortConfigPublishModeIngress) && value != string(swarm.PortConfigPublishModeHost) {
|
|
| 49 |
+ return fmt.Errorf("invalid publish mode value %s", value)
|
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ pConfig.PublishMode = swarm.PortConfigPublishMode(value) |
|
| 53 |
+ case portOptTargetPort: |
|
| 54 |
+ tPort, err := strconv.ParseUint(value, 10, 16) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ return err |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ pConfig.TargetPort = uint32(tPort) |
|
| 60 |
+ case portOptPublishedPort: |
|
| 61 |
+ pPort, err := strconv.ParseUint(value, 10, 16) |
|
| 62 |
+ if err != nil {
|
|
| 63 |
+ return err |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ pConfig.PublishedPort = uint32(pPort) |
|
| 67 |
+ default: |
|
| 68 |
+ return fmt.Errorf("invalid field key %s", key)
|
|
| 69 |
+ } |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+ if pConfig.TargetPort == 0 {
|
|
| 73 |
+ return fmt.Errorf("missing mandatory field %q", portOptTargetPort)
|
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ p.ports = append(p.ports, pConfig) |
|
| 77 |
+ return nil |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+// Type returns the type of this option |
|
| 81 |
+func (p *PortOpt) Type() string {
|
|
| 82 |
+ return "port" |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+// String returns a string repr of this option |
|
| 86 |
+func (p *PortOpt) String() string {
|
|
| 87 |
+ ports := []string{}
|
|
| 88 |
+ for _, port := range p.ports {
|
|
| 89 |
+ repr := fmt.Sprintf("%v:%v/%s/%s", port.PublishedPort, port.TargetPort, port.Protocol, port.PublishMode)
|
|
| 90 |
+ ports = append(ports, repr) |
|
| 91 |
+ } |
|
| 92 |
+ return strings.Join(ports, ", ") |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// Value returns the ports |
|
| 96 |
+func (p *PortOpt) Value() []swarm.PortConfig {
|
|
| 97 |
+ return p.ports |
|
| 98 |
+} |