Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions pkg/portutil/portutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,36 @@ func ParseFlagP(s string) ([]cni.PortMapping, error) {
if err != nil {
return nil, err
}
for i := startHostPort; i <= endHostPort; i++ {
if usedPorts[i] {
return nil, fmt.Errorf("bind for %s:%d failed: port is already allocated", ip, i)
if startPort == endPort && startHostPort != endHostPort {
// Docker-compatible behavior: a single container port with a host port
// range (e.g. "3000-3001:8080") treats the range as a pool and binds the
// container port to the first free host port in it, rather than silently
// collapsing to the first port and dropping the rest of the range.
// https://github.com/moby/moby/blob/master/daemon/libnetwork/portallocator/portallocator.go
found := false
for p := startHostPort; p <= endHostPort; p++ {
if !usedPorts[p] {
startHostPort, endHostPort = p, p
found = true
break
}
}
if !found {
return nil, fmt.Errorf("bind for %s failed: all ports in range %s are already allocated", ip, hostPort)
}
Comment on lines +124 to +126
} else {
for i := startHostPort; i <= endHostPort; i++ {
if usedPorts[i] {
return nil, fmt.Errorf("bind for %s:%d failed: port is already allocated", ip, i)
}
}
}
}
if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
if endPort != startPort {
return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
}
// Both container and host sides are ranges but of unequal length — a genuine
// mismatch (the single-container-port pool case above has already collapsed
// the host range to one port, so it does not reach here).
return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
Comment on lines 135 to +139
}

for i := int32(0); i <= (int32(endPort) - int32(startPort)); i++ {
Expand Down
17 changes: 17 additions & 0 deletions pkg/portutil/portutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,20 @@ func TestParseFlagP(t *testing.T) {
})
}
}

// TestParseFlagPHostRangePool verifies the Docker-compatible behavior for a single
// container port with a host port range (e.g. "3000-3001:8080"): the container port
// is bound to one free host port from the range, not collapsed-and-dropped. The exact
// port chosen depends on what is free in the environment, so this asserts membership
// in the range rather than a specific port.
func TestParseFlagPHostRangePool(t *testing.T) {
got, err := ParseFlagP("127.0.0.1:3000-3001:8080/tcp")
assert.NilError(t, err)
assert.Equal(t, len(got), 1)
assert.Equal(t, got[0].ContainerPort, int32(8080))
assert.Equal(t, got[0].Protocol, "tcp")
assert.Equal(t, got[0].HostIP, "127.0.0.1")
if got[0].HostPort < 3000 || got[0].HostPort > 3001 {
t.Errorf("host port %d is outside the requested range 3000-3001", got[0].HostPort)
}
Comment on lines +363 to +377
}
Loading