Skip to content

Commit 2f92077

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 2f92077

File tree

2 files changed

+150
-44
lines changed

2 files changed

+150
-44
lines changed

manager/allocator/networkallocator/portallocator.go

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,51 @@ 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 := portStateMap[p.PublishedPort]
66+
67+
// Delete state from allocatedPorts
68+
delete(portStateMap, p.PublishedPort)
69+
70+
return v
71+
}
72+
73+
// If PublishedPort == 0 and we don't have non-zero port
74+
// then we are not allocated
75+
for publishedPort, v := range portStateMap {
76+
if publishedPort != 0 {
77+
// Delete state from allocatedPorts
78+
delete(portStateMap, publishedPort)
79+
return v
80+
}
81+
}
82+
83+
return nil
84+
}
85+
4186
func newPortAllocator() (*portAllocator, error) {
4287
portSpaces := make(map[api.PortConfig_Protocol]*portSpace)
4388
for _, protocol := range []api.PortConfig_Protocol{api.ProtocolTCP, api.ProtocolUDP} {
@@ -91,40 +136,53 @@ func reconcilePortConfigs(s *api.Service) []*api.PortConfig {
91136
return s.Spec.Endpoint.Ports
92137
}
93138

94-
allocatedPorts := make(map[api.PortConfig]*api.PortConfig)
139+
portStates := allocatedPorts{}
95140
for _, portState := range s.Endpoint.Ports {
96-
if portState.PublishMode != api.PublishModeIngress {
97-
continue
141+
if portState.PublishMode == api.PublishModeIngress {
142+
portStates.addState(portState)
98143
}
99-
100-
allocatedPorts[getPortConfigKey(portState)] = portState
101144
}
102145

103146
var portConfigs []*api.PortConfig
147+
148+
// Process the portConfig with portConfig.PublishMode != api.PublishModeIngress
149+
// and PublishedPort != 0 (high priority)
104150
for _, portConfig := range s.Spec.Endpoint.Ports {
105-
// If the PublishMode is not Ingress simply pick up
106-
// the port config.
107151
if portConfig.PublishMode != api.PublishModeIngress {
152+
// If the PublishMode is not Ingress simply pick up the port config.
108153
portConfigs = append(portConfigs, portConfig)
109-
continue
110-
}
154+
} else if portConfig.PublishedPort != 0 {
155+
// Otherwise we only process PublishedPort != 0 in this round
111156

112-
portState, ok := allocatedPorts[getPortConfigKey(portConfig)]
113-
114-
// If the portConfig is exactly the same as portState
115-
// except if SwarmPort is not user-define then prefer
116-
// portState to ensure sticky allocation of the same
117-
// 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
157+
// Remove record from portState
158+
portStates.delState(portConfig)
159+
160+
// For PublishedPort != 0 prefer the portConfig
161+
portConfigs = append(portConfigs, portConfig)
124162
}
163+
}
125164

126-
// For all other cases prefer the portConfig
127-
portConfigs = append(portConfigs, portConfig)
165+
// Iterate portConfigs with PublishedPort == 0 (low priority)
166+
for _, portConfig := range s.Spec.Endpoint.Ports {
167+
// Ignore ports which are not PublishModeIngress (already processed)
168+
// And we only process PublishedPort == 0 in this round
169+
// So the following:
170+
// `portConfig.PublishMode == api.PublishModeIngress && portConfig.PublishedPort == 0`
171+
if portConfig.PublishMode == api.PublishModeIngress && portConfig.PublishedPort == 0 {
172+
// If the portConfig is exactly the same as portState
173+
// except if SwarmPort is not user-define then prefer
174+
// portState to ensure sticky allocation of the same
175+
// port that was allocated before.
176+
177+
// Remove record from portState
178+
if portState := portStates.delState(portConfig); portState != nil {
179+
portConfigs = append(portConfigs, portState)
180+
continue
181+
}
182+
183+
// For all other cases prefer the portConfig
184+
portConfigs = append(portConfigs, portConfig)
185+
}
128186
}
129187

130188
return portConfigs
@@ -213,40 +271,31 @@ func (pa *portAllocator) isPortsAllocated(s *api.Service) bool {
213271
return false
214272
}
215273

216-
allocatedPorts := make(map[api.PortConfig]*api.PortConfig)
274+
portStates := allocatedPorts{}
217275
for _, portState := range s.Endpoint.Ports {
218-
if portState.PublishMode != api.PublishModeIngress {
219-
continue
276+
if portState.PublishMode == api.PublishModeIngress {
277+
portStates.addState(portState)
220278
}
221-
222-
allocatedPorts[getPortConfigKey(portState)] = portState
223279
}
224280

281+
// Iterate portConfigs with PublishedPort != 0 (high priority)
225282
for _, portConfig := range s.Spec.Endpoint.Ports {
226283
// Ignore ports which are not PublishModeIngress
227284
if portConfig.PublishMode != api.PublishModeIngress {
228285
continue
229286
}
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 {
287+
if portConfig.PublishedPort != 0 && portStates.delState(portConfig) == nil {
236288
return false
237289
}
290+
}
238291

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
292+
// Iterate portConfigs with PublishedPort == 0 (low priority)
293+
for _, portConfig := range s.Spec.Endpoint.Ports {
294+
// Ignore ports which are not PublishModeIngress
295+
if portConfig.PublishMode != api.PublishModeIngress {
296+
continue
244297
}
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 {
298+
if portConfig.PublishedPort == 0 && portStates.delState(portConfig) == nil {
250299
return false
251300
}
252301
}

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)