Skip to content

Commit 6b7c5df

Browse files
committed
Fix issues of multiple published ports mapping to the same target port
This fix tries to address the issue raised in moby/moby#29370 where a service with multiple published ports mapping to the same target port (e.g., `--publish 5000:80 --publish 5001:80`) can't be allocated. The reason for the issue is that, `getPortConfigKey` is used for both allocated ports and configured (may or may not be allocated) ports. However, `getPortConfigKey` will not take into consideration the `PublishedPort` field, which actually could be different for different allocated ports. This fix saves a map of `portKey:portNum:portState`, instead of currently used `portKey:portState` so that multiple published ports could be processed. A test case has been added in the unit test. The newly added test case will only work with this PR. Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
1 parent 74b49ee commit 6b7c5df

File tree

2 files changed

+145
-33
lines changed

2 files changed

+145
-33
lines changed

manager/allocator/networkallocator/portallocator.go

Lines changed: 88 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,54 @@ type portSpace struct {
3838
dynamicPortSpace *idm.Idm
3939
}
4040

41+
type allocatedPorts map[api.PortConfig]map[uint32]*api.PortConfig
42+
43+
func (ps allocatedPorts) addState(p *api.PortConfig) {
44+
portKey := getPortConfigKey(p)
45+
if _, ok := ps[portKey]; !ok {
46+
ps[portKey] = make(map[uint32]*api.PortConfig)
47+
}
48+
ps[portKey][p.PublishedPort] = p
49+
}
50+
51+
func (ps allocatedPorts) delState(p *api.PortConfig) *api.PortConfig {
52+
portKey := getPortConfigKey(p)
53+
54+
portStateMap, ok := ps[portKey]
55+
56+
// If name, port, protocol values don't match then we
57+
// are not allocated.
58+
if !ok {
59+
return nil
60+
}
61+
62+
if p.PublishedPort != 0 {
63+
// If SwarmPort was user defined but the port state
64+
// SwarmPort doesn't match we are not allocated.
65+
v, ok := portStateMap[p.PublishedPort]
66+
67+
if !ok {
68+
return nil
69+
}
70+
// Dlete state from allocatedPorts
71+
delete(ps[portKey], p.PublishedPort)
72+
73+
return v
74+
}
75+
76+
// If PublishedPort == 0 and we don't have non-zero port
77+
// then we are not allocated
78+
for publishedPort, v := range portStateMap {
79+
if publishedPort != 0 {
80+
// Dlete state from allocatedPorts
81+
delete(ps[portKey], publishedPort)
82+
return v
83+
}
84+
}
85+
86+
return nil
87+
}
88+
4189
func newPortAllocator() (*portAllocator, error) {
4290
portSpaces := make(map[api.PortConfig_Protocol]*portSpace)
4391
for _, protocol := range []api.PortConfig_Protocol{api.ProtocolTCP, api.ProtocolUDP} {
@@ -73,7 +121,7 @@ func newPortSpace(protocol api.PortConfig_Protocol) (*portSpace, error) {
73121
}, nil
74122
}
75123

76-
// getPortConfigKey returns a map key for doing set operations with
124+
// getPortConfigkey returns a map key for doing set operations with
77125
// ports. The key consists of name, protocol and target port which
78126
// uniquely identifies a port within a single Endpoint.
79127
func getPortConfigKey(p *api.PortConfig) api.PortConfig {
@@ -91,40 +139,55 @@ func reconcilePortConfigs(s *api.Service) []*api.PortConfig {
91139
return s.Spec.Endpoint.Ports
92140
}
93141

94-
allocatedPorts := make(map[api.PortConfig]*api.PortConfig)
142+
portStates := allocatedPorts{}
95143
for _, portState := range s.Endpoint.Ports {
96144
if portState.PublishMode != api.PublishModeIngress {
97145
continue
98146
}
99-
100-
allocatedPorts[getPortConfigKey(portState)] = portState
147+
portStates.addState(portState)
101148
}
102149

103150
var portConfigs []*api.PortConfig
151+
152+
// Process the portConfig with portConfig.PublishMode != api.PublishModeIngress
153+
// and PublishedPort != 0 (high priority)
104154
for _, portConfig := range s.Spec.Endpoint.Ports {
105155
// If the PublishMode is not Ingress simply pick up
106156
// the port config.
107157
if portConfig.PublishMode != api.PublishModeIngress {
108158
portConfigs = append(portConfigs, portConfig)
109159
continue
110160
}
161+
if portConfig.PublishedPort != 0 {
162+
// Remove record from portState
163+
portStates.delState(portConfig)
111164

112-
portState, ok := allocatedPorts[getPortConfigKey(portConfig)]
165+
// For PublishedPort != 0 prefer the portConfig
166+
portConfigs = append(portConfigs, portConfig)
167+
}
168+
}
169+
170+
// Iterate portConfigs with PublishedPort == 0 (low priority)
171+
for _, portConfig := range s.Spec.Endpoint.Ports {
172+
// Ignore ports which are not PublishModeIngress (already processed)
173+
if portConfig.PublishMode != api.PublishModeIngress {
174+
continue
175+
}
113176

114177
// If the portConfig is exactly the same as portState
115178
// except if SwarmPort is not user-define then prefer
116179
// portState to ensure sticky allocation of the same
117180
// port that was allocated before.
118-
if ok && portConfig.Name == portState.Name &&
119-
portConfig.TargetPort == portState.TargetPort &&
120-
portConfig.Protocol == portState.Protocol &&
121-
portConfig.PublishedPort == 0 {
122-
portConfigs = append(portConfigs, portState)
123-
continue
124-
}
181+
if portConfig.PublishedPort == 0 {
182+
// Remove record from portState
183+
if portState := portStates.delState(portConfig); portState != nil {
184+
portConfigs = append(portConfigs, portState)
185+
continue
186+
}
125187

126-
// For all other cases prefer the portConfig
127-
portConfigs = append(portConfigs, portConfig)
188+
// For all other cases prefer the portConfig
189+
portConfigs = append(portConfigs, portConfig)
190+
}
128191
}
129192

130193
return portConfigs
@@ -213,40 +276,32 @@ func (pa *portAllocator) isPortsAllocated(s *api.Service) bool {
213276
return false
214277
}
215278

216-
allocatedPorts := make(map[api.PortConfig]*api.PortConfig)
279+
portStates := allocatedPorts{}
217280
for _, portState := range s.Endpoint.Ports {
218281
if portState.PublishMode != api.PublishModeIngress {
219282
continue
220283
}
221-
222-
allocatedPorts[getPortConfigKey(portState)] = portState
284+
portStates.addState(portState)
223285
}
224286

287+
// Iterate portConfigs with PublishedPort != 0 (high priority)
225288
for _, portConfig := range s.Spec.Endpoint.Ports {
226289
// Ignore ports which are not PublishModeIngress
227290
if portConfig.PublishMode != api.PublishModeIngress {
228291
continue
229292
}
230-
231-
portState, ok := allocatedPorts[getPortConfigKey(portConfig)]
232-
233-
// If name, port, protocol values don't match then we
234-
// are not allocated.
235-
if !ok {
293+
if portConfig.PublishedPort != 0 && portStates.delState(portConfig) == nil {
236294
return false
237295
}
296+
}
238297

239-
// If SwarmPort was user defined but the port state
240-
// SwarmPort doesn't match we are not allocated.
241-
if portConfig.PublishedPort != portState.PublishedPort &&
242-
portConfig.PublishedPort != 0 {
243-
return false
298+
// Iterate portConfigs with PublishedPort == 0 (low priority)
299+
for _, portConfig := range s.Spec.Endpoint.Ports {
300+
// Ignore ports which are not PublishModeIngress
301+
if portConfig.PublishMode != api.PublishModeIngress {
302+
continue
244303
}
245-
246-
// If SwarmPort was not defined by user and port state
247-
// is not initialized with a valid SwarmPort value then
248-
// we are not allocated.
249-
if portConfig.PublishedPort == 0 && portState.PublishedPort == 0 {
304+
if portConfig.PublishedPort == 0 && portStates.delState(portConfig) == nil {
250305
return false
251306
}
252307
}

manager/allocator/networkallocator/portallocator_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,63 @@ func TestIsPortsAllocated(t *testing.T) {
550550
},
551551
expect: true,
552552
},
553+
{
554+
// Endpoint and Spec.Endpoint have multiple PublishedPort
555+
// See docker/docker#29730
556+
input: &api.Service{
557+
Spec: api.ServiceSpec{
558+
Endpoint: &api.EndpointSpec{
559+
Ports: []*api.PortConfig{
560+
{
561+
Protocol: api.ProtocolTCP,
562+
TargetPort: 80,
563+
PublishedPort: 5000,
564+
},
565+
{
566+
Protocol: api.ProtocolTCP,
567+
TargetPort: 80,
568+
PublishedPort: 5001,
569+
},
570+
{
571+
Protocol: api.ProtocolTCP,
572+
TargetPort: 80,
573+
PublishedPort: 0,
574+
},
575+
{
576+
Protocol: api.ProtocolTCP,
577+
TargetPort: 80,
578+
PublishedPort: 0,
579+
},
580+
},
581+
},
582+
},
583+
Endpoint: &api.Endpoint{
584+
Ports: []*api.PortConfig{
585+
{
586+
Protocol: api.ProtocolTCP,
587+
TargetPort: 80,
588+
PublishedPort: 5000,
589+
},
590+
{
591+
Protocol: api.ProtocolTCP,
592+
TargetPort: 80,
593+
PublishedPort: 5001,
594+
},
595+
{
596+
Protocol: api.ProtocolTCP,
597+
TargetPort: 80,
598+
PublishedPort: 30000,
599+
},
600+
{
601+
Protocol: api.ProtocolTCP,
602+
TargetPort: 80,
603+
PublishedPort: 30001,
604+
},
605+
},
606+
},
607+
},
608+
expect: true,
609+
},
553610
}
554611

555612
for _, singleTest := range testCases {

0 commit comments

Comments
 (0)