api/types/container: add network port and port range types

Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Co-authored-by: Cory Snider <csnider@mirantis.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
This commit is contained in:
Austin Vazquez
2025-08-12 22:10:43 -05:00
parent dcf5db2464
commit cb3abacc52
44 changed files with 2405 additions and 729 deletions

View File

@@ -3,7 +3,6 @@ module github.com/moby/moby/api
go 1.23.0
require (
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0
github.com/google/go-cmp v0.7.0
github.com/moby/docker-image-spec v1.3.1

View File

@@ -1,5 +1,3 @@
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=

View File

@@ -1,24 +0,0 @@
package container
import "github.com/docker/go-connections/nat"
// PortRangeProto is a string containing port number and protocol in the format "80/tcp",
// or a port range and protocol in the format "80-83/tcp".
//
// It is currently an alias for [nat.Port] but may become a concrete type in a future release.
type PortRangeProto = nat.Port
// PortSet is a collection of structs indexed by [HostPort].
//
// It is currently an alias for [nat.PortSet] but may become a concrete type in a future release.
type PortSet = nat.PortSet
// PortBinding represents a binding between a Host IP address and a [HostPort].
//
// It is currently an alias for [nat.PortBinding] but may become a concrete type in a future release.
type PortBinding = nat.PortBinding
// PortMap is a collection of [PortBinding] indexed by [HostPort].
//
// It is currently an alias for [nat.PortMap] but may become a concrete type in a future release.
type PortMap = nat.PortMap

View File

@@ -0,0 +1,345 @@
package container
import (
"errors"
"fmt"
"iter"
"strconv"
"strings"
"unique"
)
// NetworkProtocol represents a network protocol for a port.
type NetworkProtocol string
const (
TCP NetworkProtocol = "tcp"
UDP NetworkProtocol = "udp"
SCTP NetworkProtocol = "sctp"
)
// Sentinel port proto value for zero Port and PortRange values.
var protoZero unique.Handle[NetworkProtocol]
// Port is a type representing a single port number and protocol in the format "<portnum>/[<proto>]".
//
// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value.
type Port struct {
num uint16
proto unique.Handle[NetworkProtocol]
}
// ParsePort parses s as a [Port].
//
// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent.
// If a port number is provided, but no protocol, the default ("tcp") protocol is returned.
func ParsePort(s string) (Port, error) {
if s == "" {
return Port{}, errors.New("invalid port: value is empty")
}
port, proto, _ := strings.Cut(s, "/")
portNum, err := parsePortNumber(port)
if err != nil {
return Port{}, fmt.Errorf("invalid port '%s': %w", port, err)
}
normalizedPortProto := normalizePortProto(proto)
return Port{num: portNum, proto: normalizedPortProto}, nil
}
// MustParsePort calls [ParsePort](s) and panics on error.
//
// It is intended for use in tests with hard-coded strings.
func MustParsePort(s string) Port {
p, err := ParsePort(s)
if err != nil {
panic(err)
}
return p
}
// PortFrom returns a [Port] with the given number and protocol.
//
// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
func PortFrom(num uint16, proto NetworkProtocol) (p Port, ok bool) {
if proto == "" {
return Port{}, false
}
normalized := normalizePortProto(string(proto))
return Port{num: num, proto: normalized}, true
}
// Num returns p's port number.
func (p Port) Num() uint16 {
return p.num
}
// Proto returns p's network protocol.
func (p Port) Proto() NetworkProtocol {
return p.proto.Value()
}
// IsZero reports whether p is the zero value.
func (p Port) IsZero() bool {
return p.proto == protoZero
}
// IsValid reports whether p is an initialized valid port (not the zero value).
func (p Port) IsValid() bool {
return p.proto != protoZero
}
// String returns a string representation of the port in the format "<portnum>/<proto>".
// If the port is the zero value, it returns "invalid port".
func (p Port) String() string {
switch p.proto {
case protoZero:
return "invalid port"
default:
return string(p.AppendTo(nil))
}
}
// AppendText implements [encoding.TextAppender] interface.
// It is the same as [Port.AppendTo] but returns an error to satisfy the interface.
func (p Port) AppendText(b []byte) ([]byte, error) {
return p.AppendTo(b), nil
}
// AppendTo appends a text encoding of p to b and returns the extended buffer.
func (p Port) AppendTo(b []byte) []byte {
if p.IsZero() {
return b
}
return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value())
}
// MarshalText implements [encoding.TextMarshaler] interface.
func (p Port) MarshalText() ([]byte, error) {
return p.AppendText(nil)
}
// UnmarshalText implements [encoding.TextUnmarshaler] interface.
func (p *Port) UnmarshalText(text []byte) error {
if len(text) == 0 {
*p = Port{}
return nil
}
port, err := ParsePort(string(text))
if err != nil {
return err
}
*p = port
return nil
}
// Range returns a [PortRange] representing the single port.
func (p Port) Range() PortRange {
return PortRange{start: p.num, end: p.num, proto: p.proto}
}
// PortSet is a collection of structs indexed by [Port].
type PortSet = map[Port]struct{}
// PortBinding represents a binding between a Host IP address and a Host Port.
type PortBinding struct {
// HostIP is the host IP Address
HostIP string `json:"HostIp"`
// HostPort is the host port number
HostPort string `json:"HostPort"`
}
// PortMap is a collection of [PortBinding] indexed by [Port].
type PortMap = map[Port][]PortBinding
// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp".
//
// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value.
type PortRange struct {
start uint16
end uint16
proto unique.Handle[NetworkProtocol]
}
// ParsePortRange parses s as a [PortRange].
//
// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent.
// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned.
func ParsePortRange(s string) (PortRange, error) {
if s == "" {
return PortRange{}, errors.New("invalid port range: value is empty")
}
portRange, proto, _ := strings.Cut(s, "/")
start, end, ok := strings.Cut(portRange, "-")
startVal, err := parsePortNumber(start)
if err != nil {
return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err)
}
portProto := normalizePortProto(proto)
if !ok || start == end {
return PortRange{start: startVal, end: startVal, proto: portProto}, nil
}
endVal, err := parsePortNumber(end)
if err != nil {
return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err)
}
if endVal < startVal {
return PortRange{}, errors.New("invalid port range: " + s)
}
return PortRange{start: startVal, end: endVal, proto: portProto}, nil
}
// MustParsePortRange calls [ParsePortRange](s) and panics on error.
// It is intended for use in tests with hard-coded strings.
func MustParsePortRange(s string) PortRange {
pr, err := ParsePortRange(s)
if err != nil {
panic(err)
}
return pr
}
// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol.
//
// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false.
func PortRangeFrom(start, end uint16, proto NetworkProtocol) (pr PortRange, ok bool) {
if end < start || proto == "" {
return PortRange{}, false
}
normalized := normalizePortProto(string(proto))
return PortRange{start: start, end: end, proto: normalized}, true
}
// Start returns pr's start port number.
func (pr PortRange) Start() uint16 {
return pr.start
}
// End returns pr's end port number.
func (pr PortRange) End() uint16 {
return pr.end
}
// Proto returns pr's network protocol.
func (pr PortRange) Proto() NetworkProtocol {
return pr.proto.Value()
}
// IsZero reports whether pr is the zero value.
func (pr PortRange) IsZero() bool {
return pr.proto == protoZero
}
// IsValid reports whether pr is an initialized valid port range (not the zero value).
func (pr PortRange) IsValid() bool {
return pr.proto != protoZero
}
// String returns a string representation of the port range in the format "<start>-<end>/<proto>" or "<portnum>/<proto>" if start == end.
// If the port range is the zero value, it returns "invalid port range".
func (pr PortRange) String() string {
switch pr.proto {
case protoZero:
return "invalid port range"
default:
return string(pr.AppendTo(nil))
}
}
// AppendText implements [encoding.TextAppender] interface.
// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface.
func (pr PortRange) AppendText(b []byte) ([]byte, error) {
return pr.AppendTo(b), nil
}
// AppendTo appends a text encoding of pr to b and returns the extended buffer.
func (pr PortRange) AppendTo(b []byte) []byte {
if pr.IsZero() {
return b
}
if pr.start == pr.end {
return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value())
}
return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value())
}
// MarshalText implements [encoding.TextMarshaler] interface.
func (pr PortRange) MarshalText() ([]byte, error) {
return pr.AppendText(nil)
}
// UnmarshalText implements [encoding.TextUnmarshaler] interface.
func (pr *PortRange) UnmarshalText(text []byte) error {
if len(text) == 0 {
*pr = PortRange{}
return nil
}
portRange, err := ParsePortRange(string(text))
if err != nil {
return err
}
*pr = portRange
return nil
}
// Range returns pr.
func (pr PortRange) Range() PortRange {
return pr
}
// All returns an iterator over all the individual ports in the range.
//
// For example:
//
// for port := range pr.All() {
// // ...
// }
func (pr PortRange) All() iter.Seq[Port] {
return func(yield func(Port) bool) {
for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ {
if !yield(Port{num: uint16(i), proto: pr.proto}) {
return
}
}
}
}
// parsePortNumber parses rawPort into an int, unwrapping strconv errors
// and returning a single "out of range" error for any value outside 065535.
func parsePortNumber(rawPort string) (uint16, error) {
if rawPort == "" {
return 0, errors.New("value is empty")
}
port, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return 0, err
}
return uint16(port), nil
}
// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent.
// If proto is not specified, it defaults to "tcp".
func normalizePortProto(proto string) unique.Handle[NetworkProtocol] {
if proto == "" {
return unique.Make(TCP)
}
proto = strings.ToLower(proto)
return unique.Make(NetworkProtocol(proto))
}

View File

@@ -0,0 +1,600 @@
package container
import (
"encoding/json"
"fmt"
"reflect"
"slices"
"strings"
"testing"
"gotest.tools/v3/assert"
)
type TestRanger interface {
Range() PortRange
}
var _ TestRanger = Port{}
var _ TestRanger = PortRange{}
func TestPort(t *testing.T) {
t.Run("Zero Value", func(t *testing.T) {
var p Port
assert.Check(t, p.IsZero())
assert.Check(t, !p.IsValid())
assert.Equal(t, p.String(), "invalid port")
t.Run("Marshal Unmarshal", func(t *testing.T) {
var p Port
bytes, err := p.MarshalText()
assert.NilError(t, err)
assert.Check(t, len(bytes) == 0)
err = p.UnmarshalText([]byte(""))
assert.NilError(t, err)
assert.Equal(t, p, Port{})
})
t.Run("JSON Marshal Unmarshal", func(t *testing.T) {
var p Port
bytes, err := json.Marshal(p)
assert.NilError(t, err)
assert.Equal(t, string(bytes), `""`)
err = json.Unmarshal([]byte(`""`), &p)
assert.NilError(t, err)
assert.Equal(t, p, Port{})
})
})
t.Run("PortFrom", func(t *testing.T) {
tests := []struct {
num uint16
proto NetworkProtocol
}{
{0, TCP},
{80, TCP},
{8080, TCP},
{65535, TCP},
{80, UDP},
{8080, SCTP},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) {
p, ok := PortFrom(tc.num, tc.proto)
assert.Check(t, ok)
assert.Equal(t, p.Num(), tc.num)
assert.Equal(t, p.Proto(), tc.proto)
})
}
t.Run("Normalize Protocol", func(t *testing.T) {
pr1 := portFrom(1234, "tcp")
pr2 := portFrom(1234, "TCP")
pr3 := portFrom(1234, "tCp")
assert.Equal(t, pr1, pr2)
assert.Equal(t, pr2, pr3)
})
negativeTests := []struct {
num uint16
proto NetworkProtocol
}{
{0, ""},
{80, ""},
}
for _, tc := range negativeTests {
t.Run(fmt.Sprintf("%d_%s", tc.num, tc.proto), func(t *testing.T) {
p, ok := PortFrom(tc.num, tc.proto)
assert.Check(t, !ok)
assert.Check(t, p.IsZero())
assert.Check(t, !p.IsValid())
assert.Equal(t, p.String(), "invalid port")
})
}
})
t.Run("ParsePort", func(t *testing.T) {
tests := []struct {
in string
port Port // output of ParsePort()
str string // output of String().
portRange PortRange // output of Range()
}{
// Zero port
{
in: "0/tcp",
port: portFrom(0, TCP),
str: "0/tcp",
portRange: portRangeFrom(0, 0, TCP),
},
// Max valid port
{
in: "65535/tcp",
port: portFrom(65535, TCP),
str: "65535/tcp",
portRange: portRangeFrom(65535, 65535, TCP),
},
// Simple valid ports
{
in: "1234/tcp",
port: portFrom(1234, TCP),
str: "1234/tcp",
portRange: portRangeFrom(1234, 1234, TCP),
},
{
in: "1234/udp",
port: portFrom(1234, UDP),
str: "1234/udp",
portRange: portRangeFrom(1234, 1234, UDP),
},
{
in: "1234/sctp",
port: portFrom(1234, SCTP),
str: "1234/sctp",
portRange: portRangeFrom(1234, 1234, SCTP),
},
// Default protocol is tcp
{
in: "1234",
port: portFrom(1234, TCP),
str: "1234/tcp",
portRange: portRangeFrom(1234, 1234, TCP),
},
// Default protocol is tcp
{
in: "1234/",
port: portFrom(1234, TCP),
str: "1234/tcp",
portRange: portRangeFrom(1234, 1234, TCP),
},
{
in: "1234/tcp:ipv6only",
port: portFrom(1234, "tcp:ipv6only"),
str: "1234/tcp:ipv6only",
portRange: portRangeFrom(1234, 1234, "tcp:ipv6only"),
},
}
for _, tc := range tests {
t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) {
got, err := ParsePort(tc.in)
assert.NilError(t, err)
assert.Equal(t, got, tc.port)
MustParsePort(tc.in) // should not panic
assert.Check(t, !got.IsZero())
assert.Check(t, got.IsValid())
// Check that ParsePort is a pure function.
got2, err := ParsePort(tc.in)
assert.NilError(t, err)
assert.Equal(t, got2, got)
// Check that ParsePort(port.String()) is the identity function.
got3, err := ParsePort(got.String())
assert.NilError(t, err)
assert.Equal(t, got3, got)
// Check String() output
s := got.String()
wants := tc.str
if wants == "" {
wants = tc.in
}
assert.Equal(t, s, wants)
js := `"` + tc.in + `"`
var jsgot Port
err = json.Unmarshal([]byte(js), &jsgot)
assert.NilError(t, err)
assert.Equal(t, jsgot, got)
jsb, err := json.Marshal(jsgot)
assert.NilError(t, err)
jswant := `"` + wants + `"`
assert.Equal(t, string(jsb), jswant)
// Check Range() output
r := got.Range()
assert.Equal(t, r, tc.portRange)
})
}
t.Run("Normalize Protocol", func(t *testing.T) {
p1 := MustParsePort("1234/tcp")
p2 := MustParsePort("1234/TCP")
p3 := MustParsePort("1234/tCp")
assert.Equal(t, p1, p2)
assert.Equal(t, p2, p3)
})
negativeTests := []string{
// Empty string
"",
// Whitespace-only string
" ",
// No port number
"/",
// No port number (protocol only)
"/tcp",
// Negative port
"-1",
// Too large port
"65536",
// Non-numeric port
"foo",
// Port range instead of single port
"1234-1240/udp",
// Port range instead of single port without protocol
"1234-1240",
// Garbage port
"asd1234/tcp",
}
for _, s := range negativeTests {
t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) {
got, err := ParsePort(s)
assert.ErrorContains(t, err, "invalid port")
assert.Check(t, got.IsZero())
assert.Check(t, !got.IsValid())
// Skip JSON unmarshalling test for empty string as that should succeed.
// See test "Zero Value" above.
if s == "" {
return
}
var jsgot Port
js := []byte(`"` + s + `"`)
err = json.Unmarshal(js, &jsgot)
assert.ErrorContains(t, err, "invalid port")
assert.Equal(t, jsgot, Port{})
})
}
})
}
func TestPortRange(t *testing.T) {
t.Run("Zero Value", func(t *testing.T) {
var pr PortRange
assert.Check(t, pr.IsZero())
assert.Check(t, !pr.IsValid())
assert.Equal(t, pr.String(), "invalid port range")
t.Run("Marshal Unmarshal", func(t *testing.T) {
var pr PortRange
bytes, err := pr.MarshalText()
assert.NilError(t, err)
assert.Check(t, len(bytes) == 0)
err = pr.UnmarshalText([]byte(""))
assert.NilError(t, err)
assert.Equal(t, pr, PortRange{})
})
t.Run("JSON Marshal Unmarshal", func(t *testing.T) {
var pr PortRange
bytes, err := json.Marshal(pr)
assert.NilError(t, err)
assert.Equal(t, string(bytes), `""`)
err = json.Unmarshal([]byte(`""`), &pr)
assert.NilError(t, err)
assert.Equal(t, pr, PortRange{})
})
})
t.Run("PortRangeFrom", func(t *testing.T) {
tests := []struct {
start uint16
end uint16
proto NetworkProtocol
}{
{0, 0, TCP},
{0, 1234, TCP},
{80, 80, TCP},
{80, 8080, TCP},
{1234, 65535, TCP},
{80, 80, UDP},
{80, 8080, SCTP},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) {
pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto)
assert.Check(t, ok)
assert.Equal(t, pr.Start(), tc.start)
assert.Equal(t, pr.End(), tc.end)
assert.Equal(t, pr.Proto(), tc.proto)
})
}
t.Run("Normalize Protocol", func(t *testing.T) {
pr1, _ := PortRangeFrom(1234, 5678, "tcp")
pr2, _ := PortRangeFrom(1234, 5678, "TCP")
pr3, _ := PortRangeFrom(1234, 5678, "tCp")
assert.Equal(t, pr1, pr2)
assert.Equal(t, pr2, pr3)
})
negativeTests := []struct {
start uint16
end uint16
proto NetworkProtocol
}{
{1234, 80, TCP}, // end < start
{0, 0, ""}, // empty protocol
}
for _, tc := range negativeTests {
t.Run(fmt.Sprintf("%d_%d_%s", tc.start, tc.end, tc.proto), func(t *testing.T) {
pr, ok := PortRangeFrom(tc.start, tc.end, tc.proto)
assert.Check(t, !ok)
assert.Check(t, pr.IsZero())
assert.Check(t, !pr.IsValid())
})
}
})
t.Run("ParsePortRange", func(t *testing.T) {
tests := []struct {
in string
portRange PortRange // output of ParsePortRange() and Range()
str string // output of String(). If "", use in.
}{
// Zero port
{
in: "0-1234/tcp",
portRange: portRangeFrom(0, 1234, TCP),
str: "0-1234/tcp",
},
// Max valid port
{
in: "1234-65535/tcp",
portRange: portRangeFrom(1234, 65535, TCP),
str: "1234-65535/tcp",
},
// Simple valid ports
{
in: "1234-4567/tcp",
portRange: portRangeFrom(1234, 4567, TCP),
str: "1234-4567/tcp",
},
{
in: "1234-4567/udp",
portRange: portRangeFrom(1234, 4567, UDP),
str: "1234-4567/udp",
},
// Default protocol is tcp
{
in: "1234-4567",
portRange: portRangeFrom(1234, 4567, TCP),
str: "1234-4567/tcp",
},
// Default protocol is tcp
{
in: "1234-4567/",
portRange: portRangeFrom(1234, 4567, TCP),
str: "1234-4567/tcp",
},
{
in: "1234/tcp",
portRange: portRangeFrom(1234, 1234, TCP),
str: "1234/tcp",
},
{
in: "1234",
portRange: portRangeFrom(1234, 1234, TCP),
str: "1234/tcp",
},
{
in: "1234-5678/tcp:ipv6only",
portRange: portRangeFrom(1234, 5678, "tcp:ipv6only"),
str: "1234-5678/tcp:ipv6only",
},
}
for _, tc := range tests {
t.Run(strings.ReplaceAll(tc.in, "/", "_"), func(t *testing.T) {
got, err := ParsePortRange(tc.in)
assert.NilError(t, err)
assert.Equal(t, got, tc.portRange)
assert.Check(t, !got.IsZero())
assert.Check(t, got.IsValid())
MustParsePortRange(tc.in) // should not panic
// Check that ParsePortRange is a pure function.
got2, err := ParsePortRange(tc.in)
assert.NilError(t, err)
assert.Equal(t, got2, got)
// Check that ParsePortRange(port.String()) is the identity function.
got3, err := ParsePortRange(got.String())
assert.NilError(t, err)
assert.Equal(t, got3, got)
// Check String() output
s := got.String()
wants := tc.str
if wants == "" {
wants = tc.in
}
assert.Equal(t, s, wants)
js := `"` + tc.in + `"`
var jsgot PortRange
err = json.Unmarshal([]byte(js), &jsgot)
assert.NilError(t, err)
assert.Equal(t, jsgot, got)
jsb, err := json.Marshal(jsgot)
assert.NilError(t, err)
jswant := `"` + wants + `"`
assert.Equal(t, string(jsb), jswant)
// Check Range() output
r := got.Range()
assert.Equal(t, r, tc.portRange)
})
t.Run("Normalize Protocol", func(t *testing.T) {
pr1 := MustParsePortRange("1234-5678/tcp")
pr2 := MustParsePortRange("1234-5678/TCP")
pr3 := MustParsePortRange("1234-5678/tCp")
assert.Equal(t, pr1, pr2)
assert.Equal(t, pr2, pr3)
})
negativeTests := []string{
// Empty string
"",
// Whitespace-only string
" ",
// No port number
"/",
// No port number (protocol only)
"/tcp",
// Negative start port
"-1-1234",
// Negative end port
"1234--1",
// Too large start port
"65536-65537",
// Too large end port
"1234-65536",
// Non-numeric start port
"foo-1234",
// Non-numeric end port
"1234-bar",
// Start port greater than end port
"1234-1000",
// Garbage port range
"asd1234-5678/tcp",
}
for _, s := range negativeTests {
t.Run(strings.ReplaceAll(s, "/", "_"), func(t *testing.T) {
got, err := ParsePortRange(s)
assert.Check(t, err != nil)
assert.Check(t, got.IsZero())
assert.Check(t, !got.IsValid())
// Skip JSON unmarshalling test for empty string as that should succeed.
// See test "Zero Value" above.
if s == "" {
return
}
var jsgot PortRange
js := []byte(`"` + s + `"`)
err = json.Unmarshal(js, &jsgot)
assert.Check(t, err != nil)
assert.Equal(t, jsgot, PortRange{})
})
}
}
})
t.Run("PortRange All()", func(t *testing.T) {
tests := []struct {
in string
want []Port
}{
{
in: "1000-1000/tcp",
want: []Port{portFrom(1000, TCP)},
},
{
in: "1000-1002/tcp",
want: []Port{portFrom(1000, TCP), portFrom(1001, TCP), portFrom(1002, TCP)},
},
{
in: "0-0/tcp",
want: []Port{portFrom(0, TCP)},
},
{
in: "65535-65535/tcp",
want: []Port{portFrom(65535, TCP)},
},
{
in: "65530-65535/tcp",
want: []Port{portFrom(65530, TCP), portFrom(65531, TCP), portFrom(65532, TCP), portFrom(65533, TCP), portFrom(65534, TCP), portFrom(65535, TCP)},
},
}
for _, tc := range tests {
pr := MustParsePortRange(tc.in)
ports := slices.Collect(pr.All())
if !reflect.DeepEqual(ports, tc.want) {
t.Errorf("PortRange.All() = %#v, want %#v", ports, tc.want)
}
}
t.Run("All() stop early", func(t *testing.T) {
want := []Port{portFrom(1000, TCP), portFrom(1001, TCP)}
pr := MustParsePortRange("1000-2000/tcp")
var ports []Port
for p := range pr.All() {
ports = append(ports, p)
if len(ports) == 2 {
break
}
}
if !reflect.DeepEqual(ports, want) {
t.Errorf("PortRange.All() = %#v, want %#v", ports, want)
}
})
})
}
func BenchmarkPortRangeAll(b *testing.B) {
b.Run("Single Port", func(b *testing.B) {
pr := MustParsePortRange("1234/tcp")
b.ResetTimer()
for i := 0; i < b.N; i++ {
var sink int64
for p := range pr.All() {
sink += int64(p.Num()) // prevent compiler optimization
}
if sink < 0 {
b.Fatal("unreachable")
}
}
})
b.Run("Range", func(b *testing.B) {
pr := MustParsePortRange("0-65535/tcp")
b.ResetTimer()
for i := 0; i < b.N; i++ {
var sink int64
for p := range pr.All() {
sink += int64(p.Num()) // prevent compiler optimization
}
if sink < 0 {
b.Fatal("unreachable")
}
}
})
}
func portFrom(num uint16, proto NetworkProtocol) Port {
p, ok := PortFrom(num, proto)
if !ok {
panic("invalid port")
}
return p
}
func portRangeFrom(start, end uint16, proto NetworkProtocol) PortRange {
pr, ok := PortRangeFrom(start, end, proto)
if !ok {
panic("invalid port range")
}
return pr
}

View File

@@ -11,12 +11,13 @@ import (
"bytes"
"context"
"fmt"
"net"
"runtime"
"sort"
"strconv"
"strings"
"github.com/containerd/platforms"
"github.com/docker/go-connections/nat"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerfile/shell"
@@ -525,7 +526,7 @@ func dispatchExpose(ctx context.Context, d dispatchRequest, c *instructions.Expo
}
c.Ports = ports
ps, _, err := nat.ParsePortSpecs(ports)
ps, _, err := parsePortSpecs(ports)
if err != nil {
return err
}
@@ -540,6 +541,154 @@ func dispatchExpose(ctx context.Context, d dispatchRequest, c *instructions.Expo
return d.builder.commit(ctx, d.state, "EXPOSE "+strings.Join(c.Ports, " "))
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L122-L144
//
// parsePortSpecs receives port specs in the format of ip:public:private/proto and parses
// these in to the internal types
func parsePortSpecs(ports []string) (map[container.Port]struct{}, map[container.Port][]container.PortBinding, error) {
var (
exposedPorts = make(map[container.Port]struct{}, len(ports))
bindings = make(map[container.Port][]container.PortBinding)
)
for _, p := range ports {
portMappings, err := parsePortSpec(p)
if err != nil {
return nil, nil, err
}
for _, pm := range portMappings {
for port, portBindings := range pm {
if _, ok := exposedPorts[port]; !ok {
exposedPorts[port] = struct{}{}
}
bindings[port] = append(bindings[port], portBindings...)
}
}
}
return exposedPorts, bindings, nil
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L172-L237
//
// parsePortSpec parses a port specification string into a slice of [container.PortMap]
func parsePortSpec(rawPort string) ([]container.PortMap, error) {
ip, hostPort, containerPort := splitParts(rawPort)
proto, containerPort := splitProtoPort(containerPort)
if containerPort == "" {
return nil, fmt.Errorf("no port specified: %s<empty>", rawPort)
}
proto = strings.ToLower(proto)
if err := validateProto(proto); err != nil {
return nil, err
}
if ip != "" && ip[0] == '[' {
// Strip [] from IPV6 addresses
rawIP, _, err := net.SplitHostPort(ip + ":")
if err != nil {
return nil, fmt.Errorf("invalid IP address %v: %w", ip, err)
}
ip = rawIP
}
if ip != "" && net.ParseIP(ip) == nil {
return nil, errors.New("invalid IP address: " + ip)
}
pr, err := container.ParsePortRange(containerPort)
if err != nil {
return nil, errors.New("invalid containerPort: " + containerPort)
}
var (
startPort = pr.Start()
endPort = pr.End()
)
var startHostPort, endHostPort uint16
if hostPort != "" {
hostPortRange, err := container.ParsePortRange(hostPort)
if err != nil {
return nil, errors.New("invalid hostPort: " + hostPort)
}
startHostPort = hostPortRange.Start()
endHostPort = hostPortRange.End()
if (endPort - startPort) != (endHostPort - startHostPort) {
// Allow host port range iff containerPort is not a range.
// In this case, use the host port range as the dynamic
// host port range to allocate into.
if endPort != startPort {
return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
}
}
}
count := endPort - startPort + 1
ports := make([]container.PortMap, 0, count)
for i := uint16(0); i < count; i++ {
hPort := ""
if hostPort != "" {
hPort = strconv.Itoa(int(startHostPort + i))
// Set hostPort to a range only if there is a single container port
// and a dynamic host port.
if count == 1 && startHostPort != endHostPort {
hPort += "-" + strconv.Itoa(int(endHostPort))
}
}
ports = append(ports, container.PortMap{
container.MustParsePort(fmt.Sprintf("%d/%s", startPort+i, proto)): []container.PortBinding{{HostIP: ip, HostPort: hPort}},
})
}
return ports, nil
}
// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L156-170
func splitParts(rawport string) (hostIP, hostPort, containerPort string) {
parts := strings.Split(rawport, ":")
switch len(parts) {
case 1:
return "", "", parts[0]
case 2:
return "", parts[0], parts[1]
case 3:
return parts[0], parts[1], parts[2]
default:
n := len(parts)
return strings.Join(parts[:n-2], ":"), parts[n-2], parts[n-1]
}
}
// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L95-L110
// splitProtoPort splits a port(range) and protocol, formatted as "<portnum>/[<proto>]"
// "<startport-endport>/[<proto>]". It returns an empty string for both if
// no port(range) is provided. If a port(range) is provided, but no protocol,
// the default ("tcp") protocol is returned.
//
// splitProtoPort does not validate or normalize the returned values.
func splitProtoPort(rawPort string) (proto string, port string) {
port, proto, _ = strings.Cut(rawPort, "/")
if port == "" {
return "", ""
}
if proto == "" {
proto = "tcp"
}
return proto, port
}
// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat.go#L112-L120
func validateProto(proto string) error {
switch proto {
case "tcp", "udp", "sctp":
// All good
return nil
default:
return errors.New("invalid proto: " + proto)
}
}
// USER foo
//
// Set the user to 'foo' for future commands and when running the

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"reflect"
"runtime"
"strings"
"testing"
@@ -335,7 +336,7 @@ func TestExpose(t *testing.T) {
assert.Assert(t, sb.state.runConfig.ExposedPorts != nil)
assert.Assert(t, is.Len(sb.state.runConfig.ExposedPorts, 1))
assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, container.PortRangeProto("80/tcp")))
assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, container.MustParsePort("80/tcp")))
}
func TestUser(t *testing.T) {
@@ -626,3 +627,648 @@ func TestDispatchUnsupportedOptions(t *testing.T) {
}
})
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L390-L499
func TestParsePortSpecs(t *testing.T) {
var (
portMap map[container.Port]struct{}
bindingMap map[container.Port][]container.PortBinding
err error
)
tcp1234 := container.MustParsePort("1234/tcp")
udp2345 := container.MustParsePort("2345/udp")
sctp3456 := container.MustParsePort("3456/sctp")
portMap, bindingMap, err = parsePortSpecs([]string{tcp1234.String(), udp2345.String(), sctp3456.String()})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[tcp1234]; !ok {
t.Fatal("1234/tcp was not parsed properly")
}
if _, ok := portMap[udp2345]; !ok {
t.Fatal("2345/udp was not parsed properly")
}
if _, ok := portMap[sctp3456]; !ok {
t.Fatal("3456/sctp was not parsed properly")
}
for portSpec, bindings := range bindingMap {
if len(bindings) != 1 {
t.Fatalf("%s should have exactly one binding", portSpec)
}
if bindings[0].HostIP != "" {
t.Fatalf("HostIP should not be set for %s", portSpec)
}
if bindings[0].HostPort != "" {
t.Fatalf("HostPort should not be set for %s", portSpec)
}
}
portMap, bindingMap, err = parsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp", "3456:3456/sctp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[tcp1234]; !ok {
t.Fatal("1234/tcp was not parsed properly")
}
if _, ok := portMap[udp2345]; !ok {
t.Fatal("2345/udp was not parsed properly")
}
if _, ok := portMap[sctp3456]; !ok {
t.Fatal("3456/sctp was not parsed properly")
}
for portSpec, bindings := range bindingMap {
_, port := splitProtoPort(portSpec.String())
if len(bindings) != 1 {
t.Fatalf("%s should have exactly one binding", portSpec)
}
if bindings[0].HostIP != "" {
t.Fatalf("HostIP should not be set for %s", portSpec)
}
if bindings[0].HostPort != port {
t.Fatalf("HostPort(%s) should be %s for %s", bindings[0].HostPort, port, portSpec)
}
}
portMap, bindingMap, err = parsePortSpecs([]string{"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp", "0.0.0.0:3456:3456/sctp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[tcp1234]; !ok {
t.Fatal("1234/tcp was not parsed properly")
}
if _, ok := portMap[udp2345]; !ok {
t.Fatal("2345/udp was not parsed properly")
}
if _, ok := portMap[sctp3456]; !ok {
t.Fatal("3456/sctp was not parsed properly")
}
for portSpec, bindings := range bindingMap {
_, port := splitProtoPort(portSpec.String())
if len(bindings) != 1 {
t.Fatalf("%s should have exactly one binding", portSpec)
}
if bindings[0].HostIP != "0.0.0.0" {
t.Fatalf("HostIP is not 0.0.0.0 for %s", portSpec)
}
if bindings[0].HostPort != port {
t.Fatalf("HostPort should be %s for %s", port, portSpec)
}
}
_, _, err = parsePortSpecs([]string{"localhost:1234:1234/tcp"})
if err == nil {
t.Fatal("Received no error while trying to parse a hostname instead of ip")
}
}
// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L244-L274
func TestParsePortSpecEmptyContainerPort(t *testing.T) {
tests := []struct {
name string
spec string
expError string
}{
{
name: "empty spec",
spec: "",
expError: `no port specified: <empty>`,
},
{
name: "empty container port",
spec: `0.0.0.0:1234-1235:/tcp`,
expError: `no port specified: 0.0.0.0:1234-1235:/tcp<empty>`,
},
{
name: "empty container port and proto",
spec: `0.0.0.0:1234-1235:`,
expError: `no port specified: 0.0.0.0:1234-1235:<empty>`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := parsePortSpec(tc.spec)
if err == nil || err.Error() != tc.expError {
t.Fatalf("expected %v, got: %v", tc.expError, err)
}
})
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L276-L302
func TestParsePortSpecFull(t *testing.T) {
portMappings, err := parsePortSpec("0.0.0.0:1234-1235:3333-3334/tcp")
if err != nil {
t.Fatalf("expected nil error, got: %v", err)
}
expected := []container.PortMap{
{
container.MustParsePort("3333/tcp"): []container.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: "1234",
},
},
},
{
container.MustParsePort("3334/tcp"): []container.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: "1235",
},
},
},
}
if !reflect.DeepEqual(expected, portMappings) {
t.Fatalf("wrong port mappings: got=%v, want=%v", portMappings, expected)
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L304-L388
func TestPartPortSpecIPV6(t *testing.T) {
type test struct {
name string
spec string
expected []container.PortMap
}
cases := []test{
{
name: "square angled IPV6 without host port",
spec: "[2001:4860:0:2001::68]::333",
expected: []container.PortMap{
{
container.MustParsePort("333/tcp"): []container.PortBinding{
{
HostIP: "2001:4860:0:2001::68",
HostPort: "",
},
},
},
},
},
{
name: "square angled IPV6 with host port",
spec: "[::1]:80:80",
expected: []container.PortMap{
{
container.MustParsePort("80/tcp"): []container.PortBinding{
{
HostIP: "::1",
HostPort: "80",
},
},
},
},
},
{
name: "IPV6 without host port",
spec: "2001:4860:0:2001::68::333",
expected: []container.PortMap{
{
container.MustParsePort("333/tcp"): []container.PortBinding{
{
HostIP: "2001:4860:0:2001::68",
HostPort: "",
},
},
},
},
},
{
name: "IPV6 with host port",
spec: "::1:80:80",
expected: []container.PortMap{
{
container.MustParsePort("80/tcp"): []container.PortBinding{
{
HostIP: "::1",
HostPort: "80",
},
},
},
},
},
{
name: ":: IPV6, without host port",
spec: "::::80",
expected: []container.PortMap{
{
container.MustParsePort("80/tcp"): []container.PortBinding{
{
HostIP: "::",
HostPort: "",
},
},
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
portMappings, err := parsePortSpec(c.spec)
if err != nil {
t.Fatalf("expected nil error, got: %v", err)
}
if !reflect.DeepEqual(c.expected, portMappings) {
t.Fatalf("wrong port mappings: got=%v, want=%v", portMappings, c.expected)
}
})
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L501-L600
func TestParsePortSpecsWithRange(t *testing.T) {
var (
portMap map[container.Port]struct{}
bindingMap map[container.Port][]container.PortBinding
err error
)
portMap, bindingMap, err = parsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp", "3456-3458/sctp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok {
t.Fatal("1234-1236/tcp was not parsed properly")
}
if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok {
t.Fatal("2345-2347/udp was not parsed properly")
}
if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok {
t.Fatal("3456-3458/sctp was not parsed properly")
}
for portSpec, bindings := range bindingMap {
if len(bindings) != 1 {
t.Fatalf("%s should have exactly one binding", portSpec)
}
if bindings[0].HostIP != "" {
t.Fatalf("HostIP should not be set for %s", portSpec)
}
if bindings[0].HostPort != "" {
t.Fatalf("HostPort should not be set for %s", portSpec)
}
}
portMap, bindingMap, err = parsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp", "3456-3458:3456-3458/sctp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok {
t.Fatal("1234-1236 was not parsed properly")
}
if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok {
t.Fatal("2345-2347 was not parsed properly")
}
if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok {
t.Fatal("3456-3458 was not parsed properly")
}
for portSpec, bindings := range bindingMap {
_, port := splitProtoPort(portSpec.String())
if len(bindings) != 1 {
t.Fatalf("%s should have exactly one binding", portSpec)
}
if bindings[0].HostIP != "" {
t.Fatalf("HostIP should not be set for %s", portSpec)
}
if bindings[0].HostPort != port {
t.Fatalf("HostPort should be %s for %s", port, portSpec)
}
}
portMap, bindingMap, err = parsePortSpecs([]string{"0.0.0.0:1234-1236:1234-1236/tcp", "0.0.0.0:2345-2347:2345-2347/udp", "0.0.0.0:3456-3458:3456-3458/sctp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[container.MustParsePort("1235/tcp")]; !ok {
t.Fatal("1234-1236 was not parsed properly")
}
if _, ok := portMap[container.MustParsePort("2346/udp")]; !ok {
t.Fatal("2345-2347 was not parsed properly")
}
if _, ok := portMap[container.MustParsePort("3456/sctp")]; !ok {
t.Fatal("3456-3458 was not parsed properly")
}
for portSpec, bindings := range bindingMap {
_, port := splitProtoPort(portSpec.String())
if len(bindings) != 1 || bindings[0].HostIP != "0.0.0.0" || bindings[0].HostPort != port {
t.Fatalf("Expect single binding to port %s but found %s", port, bindings)
}
}
_, _, err = parsePortSpecs([]string{"localhost:1234-1236:1234-1236/tcp"})
if err == nil {
t.Fatal("Received no error while trying to parse a hostname instead of ip")
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L602-L642
func TestParseNetworkOptsPrivateOnly(t *testing.T) {
ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::80"})
if err != nil {
t.Fatal(err)
}
if len(ports) != 1 {
t.Errorf("Expected 1 got %d", len(ports))
}
if len(bindings) != 1 {
t.Errorf("Expected 1 got %d", len(bindings))
}
for k := range ports {
if k.Proto() != "tcp" {
t.Errorf("Expected tcp got %s", k.Proto())
}
if k.Num() != 80 {
t.Errorf("Expected 80 got %d", k.Num())
}
b, exists := bindings[k]
if !exists {
t.Error("Binding does not exist")
}
if len(b) != 1 {
t.Errorf("Expected 1 got %d", len(b))
}
s := b[0]
if s.HostPort != "" {
t.Errorf("Expected \"\" got %s", s.HostPort)
}
if s.HostIP != "192.168.1.100" {
t.Errorf("Expected 192.168.1.100 got %s", s.HostIP)
}
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L644-L684
func TestParseNetworkOptsPublic(t *testing.T) {
ports, bindings, err := parsePortSpecs([]string{"192.168.1.100:8080:80"})
if err != nil {
t.Fatal(err)
}
if len(ports) != 1 {
t.Errorf("Expected 1 got %d", len(ports))
}
if len(bindings) != 1 {
t.Errorf("Expected 1 got %d", len(bindings))
}
for k := range ports {
if k.Proto() != "tcp" {
t.Errorf("Expected tcp got %s", k.Proto())
}
if k.Num() != 80 {
t.Errorf("Expected 80 got %d", k.Num())
}
b, exists := bindings[k]
if !exists {
t.Error("Binding does not exist")
}
if len(b) != 1 {
t.Errorf("Expected 1 got %d", len(b))
}
s := b[0]
if s.HostPort != "8080" {
t.Errorf("Expected 8080 got %s", s.HostPort)
}
if s.HostIP != "192.168.1.100" {
t.Errorf("Expected 192.168.1.100 got %s", s.HostIP)
}
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L686-L701
func TestParseNetworkOptsPublicNoPort(t *testing.T) {
ports, bindings, err := parsePortSpecs([]string{"192.168.1.100"})
if err == nil {
t.Error("Expected error Invalid containerPort")
}
if ports != nil {
t.Errorf("Expected nil got %s", ports)
}
if bindings != nil {
t.Errorf("Expected nil got %s", bindings)
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L703-L717
func TestParseNetworkOptsNegativePorts(t *testing.T) {
ports, bindings, err := parsePortSpecs([]string{"192.168.1.100:-1:-1"})
if err == nil {
t.Error("Expected error Invalid containerPort")
}
if len(ports) != 0 {
t.Errorf("Expected 0 got %d: %#v", len(ports), ports)
}
if len(bindings) != 0 {
t.Errorf("Expected 0 got %d: %#v", len(bindings), bindings)
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L719-L759
func TestParseNetworkOptsUdp(t *testing.T) {
ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::6000/udp"})
if err != nil {
t.Fatal(err)
}
if len(ports) != 1 {
t.Errorf("Expected 1 got %d: %#v", len(ports), ports)
}
if len(bindings) != 1 {
t.Errorf("Expected 1 got %d", len(bindings))
}
for k := range ports {
if k.Proto() != "udp" {
t.Errorf("Expected udp got %s", k.Proto())
}
if k.Num() != 6000 {
t.Errorf("Expected 6000 got %d", k.Num())
}
b, exists := bindings[k]
if !exists {
t.Error("Binding does not exist")
}
if len(b) != 1 {
t.Errorf("Expected 1 got %d", len(b))
}
s := b[0]
if s.HostPort != "" {
t.Errorf("Expected \"\" got %s", s.HostPort)
}
if s.HostIP != "192.168.1.100" {
t.Errorf("Expected 192.168.1.100 got %s", s.HostIP)
}
}
}
// Copied and modified from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L761-L801
func TestParseNetworkOptsSctp(t *testing.T) {
ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::6000/sctp"})
if err != nil {
t.Fatal(err)
}
if len(ports) != 1 {
t.Errorf("Expected 1 got %d: %#v", len(ports), ports)
}
if len(bindings) != 1 {
t.Errorf("Expected 1 got %d: %#v", len(bindings), bindings)
}
for k := range ports {
if k.Proto() != "sctp" {
t.Errorf("Expected sctp got %s", k.Proto())
}
if k.Num() != 6000 {
t.Errorf("Expected 6000 got %d", k.Num())
}
b, exists := bindings[k]
if !exists {
t.Error("Binding does not exist")
}
if len(b) != 1 {
t.Errorf("Expected 1 got %d", len(b))
}
s := b[0]
if s.HostPort != "" {
t.Errorf("Expected \"\" got %s", s.HostPort)
}
if s.HostIP != "192.168.1.100" {
t.Errorf("Expected 192.168.1.100 got %s", s.HostIP)
}
}
}
// Copied from https://github.com/docker/go-connections/blob/c296721c0d56d3acad2973376ded214103a4fd2e/nat/nat_test.go#L146-L242
func TestSplitProtoPort(t *testing.T) {
tests := []struct {
doc string
input string
expPort string
expProto string
}{
{
doc: "empty value",
},
{
doc: "zero value",
input: "0",
expPort: "0",
expProto: "tcp",
},
{
doc: "empty port",
input: "/udp",
expPort: "",
expProto: "",
},
{
doc: "single port",
input: "1234",
expPort: "1234",
expProto: "tcp",
},
{
doc: "single port with empty protocol",
input: "1234/",
expPort: "1234",
expProto: "tcp",
},
{
doc: "single port with protocol",
input: "1234/udp",
expPort: "1234",
expProto: "udp",
},
{
doc: "port range",
input: "80-8080",
expPort: "80-8080",
expProto: "tcp",
},
{
doc: "port range with empty protocol",
input: "80-8080/",
expPort: "80-8080",
expProto: "tcp",
},
{
doc: "port range with protocol",
input: "80-8080/udp",
expPort: "80-8080",
expProto: "udp",
},
// SplitProtoPort currently does not validate or normalize, so these are expected returns
{
doc: "negative value",
input: "-1",
expPort: "-1",
expProto: "tcp",
},
{
doc: "uppercase protocol",
input: "1234/UDP",
expPort: "1234",
expProto: "UDP",
},
{
doc: "any value",
input: "any port value",
expPort: "any port value",
expProto: "tcp",
},
{
doc: "any value with protocol",
input: "any port value/any proto value",
expPort: "any port value",
expProto: "any proto value",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
proto, port := splitProtoPort(tc.input)
if proto != tc.expProto {
t.Errorf("expected proto %s, got %s", tc.expProto, proto)
}
if port != tc.expPort {
t.Errorf("expected port %s, got %s", tc.expPort, port)
}
})
}
}

View File

@@ -137,8 +137,8 @@ func fullMutableRunConfig() *container.Config {
Cmd: []string{"command", "arg1"},
Env: []string{"env1=foo", "env2=bar"},
ExposedPorts: container.PortSet{
"1000/tcp": {},
"1001/tcp": {},
container.MustParsePort("1000/tcp"): {},
container.MustParsePort("1001/tcp"): {},
},
Volumes: map[string]struct{}{
"one": {},
@@ -161,7 +161,7 @@ func TestDeepCopyRunConfig(t *testing.T) {
ctrCfg.Cmd[1] = "arg2"
ctrCfg.Env[1] = "env2=new"
ctrCfg.ExposedPorts["10002"] = struct{}{}
ctrCfg.ExposedPorts[container.MustParsePort("10002")] = struct{}{}
ctrCfg.Volumes["three"] = struct{}{}
ctrCfg.Entrypoint[1] = "arg2"
ctrCfg.OnBuild[0] = "start"

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"net"
"strconv"
"strings"
@@ -150,7 +151,15 @@ func (c *containerConfig) portBindings() container.PortMap {
continue
}
port := container.PortRangeProto(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String())))
if portConfig.TargetPort > math.MaxUint16 {
continue
}
port, ok := container.PortFrom(uint16(portConfig.TargetPort), container.NetworkProtocol(portConfig.Protocol.String()))
if !ok {
continue
}
binding := []container.PortBinding{
{},
}
@@ -176,8 +185,8 @@ func (c *containerConfig) init() *bool {
return &init
}
func (c *containerConfig) exposedPorts() map[container.PortRangeProto]struct{} {
exposedPorts := make(map[container.PortRangeProto]struct{})
func (c *containerConfig) exposedPorts() map[container.Port]struct{} {
exposedPorts := make(map[container.Port]struct{})
if c.task.Endpoint == nil {
return exposedPorts
}
@@ -187,7 +196,15 @@ func (c *containerConfig) exposedPorts() map[container.PortRangeProto]struct{} {
continue
}
port := container.PortRangeProto(fmt.Sprintf("%d/%s", portConfig.TargetPort, strings.ToLower(portConfig.Protocol.String())))
if portConfig.TargetPort > math.MaxUint16 {
continue
}
port, ok := container.PortFrom(uint16(portConfig.TargetPort), container.NetworkProtocol(portConfig.Protocol.String()))
if !ok {
continue
}
exposedPorts[port] = struct{}{}
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"
cerrdefs "github.com/containerd/errdefs"
@@ -643,27 +642,17 @@ func parsePortStatus(ctnr container.InspectResponse) (*api.PortStatus, error) {
func parsePortMap(portMap container.PortMap) ([]*api.PortConfig, error) {
exposedPorts := make([]*api.PortConfig, 0, len(portMap))
for portProtocol, mapping := range portMap {
p, proto, ok := strings.Cut(string(portProtocol), "/")
if !ok {
return nil, fmt.Errorf("invalid port mapping: %s", portProtocol)
}
port, err := strconv.ParseUint(p, 10, 16)
if err != nil {
return nil, err
}
for port, mapping := range portMap {
var protocol api.PortConfig_Protocol
switch strings.ToLower(proto) {
case "tcp":
switch port.Proto() {
case container.TCP:
protocol = api.ProtocolTCP
case "udp":
case container.UDP:
protocol = api.ProtocolUDP
case "sctp":
case container.SCTP:
protocol = api.ProtocolSCTP
default:
return nil, fmt.Errorf("invalid protocol: %s", proto)
return nil, fmt.Errorf("invalid protocol: %s", port.Proto())
}
for _, binding := range mapping {
@@ -677,7 +666,7 @@ func parsePortMap(portMap container.PortMap) ([]*api.PortConfig, error) {
exposedPorts = append(exposedPorts, &api.PortConfig{
PublishMode: api.PublishModeHost,
Protocol: protocol,
TargetPort: uint32(port),
TargetPort: uint32(port.Num()),
PublishedPort: uint32(hostPort),
})
}

View File

@@ -12,7 +12,6 @@ import (
"time"
"github.com/containerd/log"
"github.com/docker/go-connections/nat"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
networktypes "github.com/moby/moby/api/types/network"
@@ -375,13 +374,17 @@ func validateHealthCheck(healthConfig *containertypes.HealthConfig) error {
func validatePortBindings(ports containertypes.PortMap) error {
for port := range ports {
_, portStr := nat.SplitProtoPort(string(port))
if _, err := nat.ParsePort(portStr); err != nil {
return errors.Errorf("invalid port specification: %q", portStr)
if !port.IsValid() {
return errors.Errorf("invalid port specification: %q", port.String())
}
for _, pb := range ports[port] {
_, err := nat.NewPort(nat.SplitProtoPort(pb.HostPort))
if err != nil {
if pb.HostPort == "" {
// Empty HostPort means to map to an ephemeral port.
continue
}
if _, err := containertypes.ParsePortRange(pb.HostPort); err != nil {
return errors.Errorf("invalid port specification: %q", pb.HostPort)
}
}

View File

@@ -10,7 +10,6 @@ import (
"time"
"github.com/containerd/log"
"github.com/docker/go-connections/nat"
memdb "github.com/hashicorp/go-memdb"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
@@ -391,29 +390,24 @@ func (v *View) transform(ctr *Container) *Snapshot {
}
}
for p, bindings := range ctr.NetworkSettings.Ports {
proto, port := nat.SplitProtoPort(string(p))
p, err := nat.ParsePort(port)
if err != nil {
log.G(context.TODO()).WithError(err).Warn("invalid port map")
continue
}
if len(bindings) == 0 {
snapshot.Ports = append(snapshot.Ports, container.PortSummary{
PrivatePort: uint16(p),
Type: proto,
PrivatePort: p.Num(),
Type: string(p.Proto()),
})
continue
}
for _, binding := range bindings {
h, err := nat.ParsePort(binding.HostPort)
// TODO(thaJeztah): if this is always a port/proto (no range), we can simplify this to [container.ParsePort].
h, err := container.ParsePortRange(binding.HostPort)
if err != nil {
log.G(context.TODO()).WithError(err).Warn("invalid host port map")
continue
}
snapshot.Ports = append(snapshot.Ports, container.PortSummary{
PrivatePort: uint16(p),
PublicPort: uint16(h),
Type: proto,
PrivatePort: p.Num(),
PublicPort: h.Start(),
Type: string(p.Proto()),
IP: binding.HostIP,
})
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"maps"
"net"
"net/netip"
"os"
@@ -15,7 +14,6 @@ import (
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/docker/go-connections/nat"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
networktypes "github.com/moby/moby/api/types/network"
@@ -112,51 +110,56 @@ func buildSandboxOptions(cfg *config.Config, ctr *container.Container) ([]libnet
}
}
// Create a deep copy (as [nat.SortPortMap] mutates the map).
// Not using a maps.Clone here, as that won't dereference the
// slice (PortMap is a map[Port][]PortBinding).
bindings := make(containertypes.PortMap)
portBindings := make(containertypes.PortMap, len(ctr.HostConfig.PortBindings))
for p, b := range ctr.HostConfig.PortBindings {
bindings[p] = slices.Clone(b)
portBindings[p] = slices.Clone(b)
}
ports := slices.Collect(maps.Keys(ctr.Config.ExposedPorts))
nat.SortPortMap(ports, bindings)
for p := range ctr.Config.ExposedPorts {
if _, ok := portBindings[p]; !ok {
// Create nil entries for exposed but un-mapped ports.
portBindings[p] = nil
}
}
var (
publishedPorts []types.PortBinding
exposedPorts []types.TransportPort
)
for _, port := range ports {
portProto := types.ParseProtocol(port.Proto())
portNum := uint16(port.Int())
for port, bindings := range portBindings {
protocol := types.ParseProtocol(string(port.Proto()))
exposedPorts = append(exposedPorts, types.TransportPort{
Proto: portProto,
Port: portNum,
Proto: protocol,
Port: port.Num(),
})
for _, binding := range bindings[port] {
newP, err := nat.NewPort(nat.SplitProtoPort(binding.HostPort))
var portStart, portEnd int
if err == nil {
portStart, portEnd, err = newP.Range()
}
if err != nil {
return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding.HostPort, err)
for _, binding := range bindings {
var (
portRange containertypes.PortRange
err error
)
// Empty HostPort means to map to an ephemeral port.
if binding.HostPort != "" {
portRange, err = containertypes.ParsePortRange(binding.HostPort)
if err != nil {
return nil, fmt.Errorf("error parsing HostPort value(%s):%v", binding.HostPort, err)
}
}
publishedPorts = append(publishedPorts, types.PortBinding{
Proto: portProto,
Port: portNum,
Proto: protocol,
Port: port.Num(),
HostIP: net.ParseIP(binding.HostIP),
HostPort: uint16(portStart),
HostPortEnd: uint16(portEnd),
HostPort: portRange.Start(),
HostPortEnd: portRange.End(),
})
}
if ctr.HostConfig.PublishAllPorts && len(bindings[port]) == 0 {
if ctr.HostConfig.PublishAllPorts && len(bindings) == 0 {
publishedPorts = append(publishedPorts, types.PortBinding{
Proto: portProto,
Port: portNum,
Proto: protocol,
Port: port.Num(),
})
}
}

View File

@@ -19,7 +19,7 @@ func TestContainerWarningHostAndPublishPorts(t *testing.T) {
}{
{ports: containertypes.PortMap{}},
{ports: containertypes.PortMap{
"8080": []containertypes.PortBinding{{HostPort: "8989"}},
containertypes.MustParsePort("8080"): []containertypes.PortBinding{{HostPort: "8989"}},
}, warnings: []string{"Published ports are discarded when using host network mode"}},
}
muteLogs(t)

View File

@@ -12,7 +12,7 @@ import (
func TestContainerConfigToDockerImageConfig(t *testing.T) {
ociCFG := containerConfigToDockerOCIImageConfig(&container.Config{
ExposedPorts: container.PortSet{
"80/tcp": struct{}{},
container.MustParsePort("80/tcp"): struct{}{},
},
})

View File

@@ -80,7 +80,7 @@ func containerConfigToDockerOCIImageConfig(cfg *container.Config) imagespec.Dock
if len(cfg.ExposedPorts) > 0 {
ociCfg.ExposedPorts = map[string]struct{}{}
for k := range cfg.ExposedPorts {
ociCfg.ExposedPorts[string(k)] = struct{}{}
ociCfg.ExposedPorts[k.String()] = struct{}{}
}
}
ext.Healthcheck = cfg.Healthcheck
@@ -97,7 +97,9 @@ func containerConfigToDockerOCIImageConfig(cfg *container.Config) imagespec.Dock
func dockerOCIImageConfigToContainerConfig(cfg imagespec.DockerOCIImageConfig) *container.Config {
exposedPorts := make(container.PortSet, len(cfg.ExposedPorts))
for k := range cfg.ExposedPorts {
exposedPorts[container.PortRangeProto(k)] = struct{}{}
if p, err := container.ParsePort(k); err == nil {
exposedPorts[p] = struct{}{}
}
}
return &container.Config{

View File

@@ -221,8 +221,8 @@ func TestContainerInitDNS(t *testing.T) {
func TestMerge(t *testing.T) {
configImage := &containertypes.Config{
ExposedPorts: containertypes.PortSet{
"1111/tcp": struct{}{},
"2222/tcp": struct{}{},
containertypes.MustParsePort("1111/tcp"): struct{}{},
containertypes.MustParsePort("2222/tcp"): struct{}{},
},
Env: []string{"VAR1=1", "VAR2=2"},
Volumes: map[string]struct{}{
@@ -233,8 +233,8 @@ func TestMerge(t *testing.T) {
configUser := &containertypes.Config{
ExposedPorts: containertypes.PortSet{
"2222/tcp": struct{}{},
"3333/tcp": struct{}{},
containertypes.MustParsePort("2222/tcp"): struct{}{},
containertypes.MustParsePort("3333/tcp"): struct{}{},
},
Env: []string{"VAR2=3", "VAR3=3"},
Volumes: map[string]struct{}{
@@ -250,7 +250,7 @@ func TestMerge(t *testing.T) {
t.Fatalf("Expected 3 ExposedPorts, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts))
}
for portSpecs := range configUser.ExposedPorts {
if portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" {
if portSpecs.Num() != 1111 && portSpecs.Num() != 2222 && portSpecs.Num() != 3333 {
t.Fatalf("Expected 1111 or 2222 or 3333, found %s", portSpecs)
}
}
@@ -273,7 +273,9 @@ func TestMerge(t *testing.T) {
}
configImage2 := &containertypes.Config{
ExposedPorts: map[containertypes.PortRangeProto]struct{}{"0/tcp": {}},
ExposedPorts: map[containertypes.Port]struct{}{
containertypes.MustParsePort("0/tcp"): {},
},
}
if err := merge(configUser, configImage2); err != nil {
@@ -284,7 +286,7 @@ func TestMerge(t *testing.T) {
t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts))
}
for portSpecs := range configUser.ExposedPorts {
if portSpecs.Port() != "0" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" {
if portSpecs.Num() != 0 && portSpecs.Num() != 1111 && portSpecs.Num() != 2222 && portSpecs.Num() != 3333 {
t.Fatalf("Expected %q or %q or %q or %q, found %s", 0, 1111, 2222, 3333, portSpecs)
}
}

View File

@@ -26,7 +26,7 @@ func containerConfigToDockerOCIImageConfig(cfg *container.Config) imagespec.Dock
if len(cfg.ExposedPorts) > 0 {
ociCfg.ExposedPorts = map[string]struct{}{}
for k, v := range cfg.ExposedPorts {
ociCfg.ExposedPorts[string(k)] = v
ociCfg.ExposedPorts[k.String()] = v
}
}
ext.Healthcheck = cfg.Healthcheck

View File

@@ -12,17 +12,17 @@ import (
func TestCompare(t *testing.T) {
ports1 := container.PortSet{
"1111/tcp": struct{}{},
"2222/tcp": struct{}{},
container.MustParsePort("1111/tcp"): struct{}{},
container.MustParsePort("2222/tcp"): struct{}{},
}
ports2 := container.PortSet{
"3333/tcp": struct{}{},
"4444/tcp": struct{}{},
container.MustParsePort("3333/tcp"): struct{}{},
container.MustParsePort("4444/tcp"): struct{}{},
}
ports3 := container.PortSet{
"1111/tcp": struct{}{},
"2222/tcp": struct{}{},
"5555/tcp": struct{}{},
container.MustParsePort("1111/tcp"): struct{}{},
container.MustParsePort("2222/tcp"): struct{}{},
container.MustParsePort("5555/tcp"): struct{}{},
}
volumes1 := map[string]struct{}{
"/test1": {},

View File

@@ -1,11 +1,13 @@
package links
import (
"cmp"
"fmt"
"maps"
"path"
"slices"
"strings"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
)
@@ -20,21 +22,18 @@ type Link struct {
// Child environments variables
ChildEnvironment []string
// Child exposed ports
Ports []container.PortRangeProto // TODO(thaJeztah): can we use []string here, or do we need the features of nat.Port?
Ports []container.Port
}
// EnvVars generates environment variables for the linked container
// for the Link with the given options.
func EnvVars(parentIP, childIP, name string, env []string, exposedPorts map[container.PortRangeProto]struct{}) []string {
func EnvVars(parentIP, childIP, name string, env []string, exposedPorts map[container.Port]struct{}) []string {
return NewLink(parentIP, childIP, name, env, exposedPorts).ToEnv()
}
// NewLink initializes a new Link struct with the provided options.
func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[container.PortRangeProto]struct{}) *Link {
ports := make([]container.PortRangeProto, 0, len(exposedPorts))
for p := range exposedPorts {
ports = append(ports, p)
}
func NewLink(parentIP, childIP, name string, env []string, exposedPorts map[container.Port]struct{}) *Link {
ports := slices.Collect(maps.Keys(exposedPorts))
return &Link{
Name: name,
@@ -53,27 +52,26 @@ func (l *Link) ToEnv() []string {
alias := strings.ReplaceAll(strings.ToUpper(n), "-", "_")
// sort the ports so that we can bulk the continuous ports together
nat.Sort(l.Ports, withTCPPriority)
slices.SortFunc(l.Ports, withTCPPriority)
var pStart, pEnd container.PortRangeProto
env := make([]string, 0, 1+len(l.Ports)*4)
var pStart, pEnd container.Port
for i, p := range l.Ports {
if i == 0 {
pStart, pEnd = p, p
env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port()))
env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%d", alias, p.Proto(), l.ChildIP, p.Num()))
}
// These env-vars are produced for every port, regardless if they're
// part of a port-range.
prefix := fmt.Sprintf("%s_PORT_%s_%s", alias, p.Port(), strings.ToUpper(p.Proto()))
env = append(env, fmt.Sprintf("%s=%s://%s:%s", prefix, p.Proto(), l.ChildIP, p.Port()))
// These env-vars are produced for every port, regardless if they're part of a port-range.
prefix := fmt.Sprintf("%s_PORT_%d_%s", alias, p.Num(), strings.ToUpper(string(p.Proto())))
env = append(env, fmt.Sprintf("%s=%s://%s:%d", prefix, p.Proto(), l.ChildIP, p.Num()))
env = append(env, fmt.Sprintf("%s_ADDR=%s", prefix, l.ChildIP))
env = append(env, fmt.Sprintf("%s_PORT=%s", prefix, p.Port()))
env = append(env, fmt.Sprintf("%s_PORT=%d", prefix, p.Num()))
env = append(env, fmt.Sprintf("%s_PROTO=%s", prefix, p.Proto()))
// Detect whether this port is part of a range (consecutive port number
// and same protocol).
if p.Int() == pEnd.Int()+1 && strings.EqualFold(p.Proto(), pStart.Proto()) {
// Detect whether this port is part of a range (consecutive port number and same protocol).
if p.Num() == pEnd.Num()+1 && p.Proto() == pEnd.Proto() {
pEnd = p
if i < len(l.Ports)-1 {
continue
@@ -81,11 +79,11 @@ func (l *Link) ToEnv() []string {
}
if pEnd != pStart {
prefix = fmt.Sprintf("%s_PORT_%s_%s", alias, pStart.Port(), strings.ToUpper(pStart.Proto()))
env = append(env, fmt.Sprintf("%s_START=%s://%s:%s", prefix, pStart.Proto(), l.ChildIP, pStart.Port()))
env = append(env, fmt.Sprintf("%s_PORT_START=%s", prefix, pStart.Port()))
env = append(env, fmt.Sprintf("%s_END=%s://%s:%s", prefix, pEnd.Proto(), l.ChildIP, pEnd.Port()))
env = append(env, fmt.Sprintf("%s_PORT_END=%s", prefix, pEnd.Port()))
prefix = fmt.Sprintf("%s_PORT_%d_%s", alias, pStart.Num(), strings.ToUpper(string(pStart.Proto())))
env = append(env, fmt.Sprintf("%s_START=%s://%s:%d", prefix, pStart.Proto(), l.ChildIP, pStart.Num()))
env = append(env, fmt.Sprintf("%s_PORT_START=%d", prefix, pStart.Num()))
env = append(env, fmt.Sprintf("%s_END=%s://%s:%d", prefix, pEnd.Proto(), l.ChildIP, pEnd.Num()))
env = append(env, fmt.Sprintf("%s_PORT_END=%d", prefix, pEnd.Num()))
}
// Reset for next range (if any)
@@ -113,17 +111,15 @@ func (l *Link) ToEnv() []string {
// withTCPPriority prioritizes ports using TCP over other protocols before
// comparing port-number and protocol.
func withTCPPriority(ip, jp container.PortRangeProto) bool {
if strings.EqualFold(ip.Proto(), jp.Proto()) {
return ip.Int() < jp.Int()
func withTCPPriority(ip, jp container.Port) int {
if ip.Proto() == jp.Proto() {
return cmp.Compare(ip.Num(), jp.Num())
}
if strings.EqualFold(ip.Proto(), "tcp") {
return true
if ip.Proto() == container.TCP {
return -1
}
if strings.EqualFold(jp.Proto(), "tcp") {
return false
if jp.Proto() == container.TCP {
return 1
}
return strings.ToLower(ip.Proto()) < strings.ToLower(jp.Proto())
return cmp.Compare(ip.Proto(), jp.Proto())
}

View File

@@ -1,17 +1,18 @@
package links
import (
"slices"
"sort"
"testing"
"github.com/docker/go-connections/nat"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"gotest.tools/v3/assert"
)
func TestLinkNaming(t *testing.T) {
actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker-1", nil, container.PortSet{
"6379/tcp": struct{}{},
container.MustParsePort("6379/tcp"): struct{}{},
})
expectedEnv := []string{
@@ -28,23 +29,25 @@ func TestLinkNaming(t *testing.T) {
}
func TestLinkNew(t *testing.T) {
tcp6379 := container.MustParsePort("6379/tcp")
link := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", nil, container.PortSet{
"6379/tcp": struct{}{},
tcp6379: struct{}{},
})
expected := &Link{
Name: "/db/docker",
ParentIP: "172.0.17.3",
ChildIP: "172.0.17.2",
Ports: []container.PortRangeProto{"6379/tcp"},
Ports: []container.Port{tcp6379},
}
assert.DeepEqual(t, expected, link)
assert.DeepEqual(t, expected, link, cmpopts.EquateComparable(container.Port{}))
}
func TestLinkEnv(t *testing.T) {
tcp6379 := container.MustParsePort("6379/tcp")
actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{
"6379/tcp": struct{}{},
tcp6379: struct{}{},
})
expectedEnv := []string{
@@ -64,39 +67,39 @@ func TestLinkEnv(t *testing.T) {
// TestSortPorts verifies that ports are sorted with TCP taking priority,
// and ports with the same protocol to be sorted by port.
func TestSortPorts(t *testing.T) {
ports := []container.PortRangeProto{
"6379/tcp",
"6376/udp",
"6380/tcp",
"6376/sctp",
"6381/tcp",
"6381/udp",
"6375/udp",
"6375/sctp",
ports := []container.Port{
container.MustParsePort("6379/tcp"),
container.MustParsePort("6376/udp"),
container.MustParsePort("6380/tcp"),
container.MustParsePort("6376/sctp"),
container.MustParsePort("6381/tcp"),
container.MustParsePort("6381/udp"),
container.MustParsePort("6375/udp"),
container.MustParsePort("6375/sctp"),
}
expected := []container.PortRangeProto{
"6379/tcp",
"6380/tcp",
"6381/tcp",
"6375/sctp",
"6376/sctp",
"6375/udp",
"6376/udp",
"6381/udp",
expected := []container.Port{
container.MustParsePort("6379/tcp"),
container.MustParsePort("6380/tcp"),
container.MustParsePort("6381/tcp"),
container.MustParsePort("6375/sctp"),
container.MustParsePort("6376/sctp"),
container.MustParsePort("6375/udp"),
container.MustParsePort("6376/udp"),
container.MustParsePort("6381/udp"),
}
nat.Sort(ports, withTCPPriority)
assert.DeepEqual(t, expected, ports)
slices.SortFunc(ports, withTCPPriority)
assert.DeepEqual(t, expected, ports, cmpopts.EquateComparable(container.Port{}))
}
func TestLinkMultipleEnv(t *testing.T) {
actual := EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{
"6300/udp": struct{}{},
"6379/tcp": struct{}{},
"6380/tcp": struct{}{},
"6381/tcp": struct{}{},
"6382/udp": struct{}{},
container.MustParsePort("6300/udp"): struct{}{},
container.MustParsePort("6379/tcp"): struct{}{},
container.MustParsePort("6380/tcp"): struct{}{},
container.MustParsePort("6381/tcp"): struct{}{},
container.MustParsePort("6382/udp"): struct{}{},
})
expectedEnv := []string{
@@ -143,11 +146,11 @@ func BenchmarkLinkMultipleEnv(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = EnvVars("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, container.PortSet{
"6300/udp": struct{}{},
"6379/tcp": struct{}{},
"6380/tcp": struct{}{},
"6381/tcp": struct{}{},
"6382/udp": struct{}{},
container.MustParsePort("6300/udp"): struct{}{},
container.MustParsePort("6379/tcp"): struct{}{},
container.MustParsePort("6380/tcp"): struct{}{},
container.MustParsePort("6381/tcp"): struct{}{},
container.MustParsePort("6382/udp"): struct{}{},
})
}
}

View File

@@ -11,7 +11,6 @@ import (
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/docker/go-connections/nat"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/v2/daemon/container"
@@ -404,13 +403,12 @@ func portOp(key string, filter map[string]bool) func(value string) error {
return fmt.Errorf("filter for '%s' should not contain ':': %s", key, value)
}
// support two formats, original format <portnum>/[<proto>] or <startport-endport>/[<proto>]
proto, portRange := nat.SplitProtoPort(value)
start, end, err := nat.ParsePortRange(portRange)
portRange, err := containertypes.ParsePortRange(value)
if err != nil {
return fmt.Errorf("error while looking up for %s %s: %s", key, value, err)
}
for portNum := start; portNum <= end; portNum++ {
filter[fmt.Sprintf("%d/%s", portNum, proto)] = true
for p := range portRange.All() {
filter[p.String()] = true
}
return nil
}

View File

@@ -4,17 +4,14 @@ import (
"context"
"errors"
"fmt"
"maps"
"net"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
"sync"
"github.com/containerd/log"
"github.com/docker/go-connections/nat"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
networktypes "github.com/moby/moby/api/types/network"
@@ -960,51 +957,44 @@ func buildPortsRelatedCreateEndpointOptions(c *container.Container, n *libnetwor
return nil, nil
}
// Create a deep copy (as [nat.SortPortMap] mutates the map).
// Not using a maps.Clone here, as that won't dereference the
// slice (PortMap is a map[Port][]PortBinding).
bindings := make(containertypes.PortMap)
for p, b := range c.HostConfig.PortBindings {
bindings[p] = slices.Clone(b)
}
ports := slices.Collect(maps.Keys(bindings))
nat.SortPortMap(ports, bindings)
var (
exposedPorts []lntypes.TransportPort
publishedPorts []lntypes.PortBinding
)
for _, port := range ports {
portProto := lntypes.ParseProtocol(port.Proto())
portNum := uint16(port.Int())
for p, bindings := range c.HostConfig.PortBindings {
protocol := lntypes.ParseProtocol(string(p.Proto()))
exposedPorts = append(exposedPorts, lntypes.TransportPort{
Proto: portProto,
Port: portNum,
Proto: protocol,
Port: p.Num(),
})
for _, binding := range bindings[port] {
newP, err := nat.NewPort(nat.SplitProtoPort(binding.HostPort))
var portStart, portEnd int
if err == nil {
portStart, portEnd, err = newP.Range()
}
if err != nil {
return nil, fmt.Errorf("error parsing HostPort value (%s): %w", binding.HostPort, err)
for _, binding := range bindings {
var (
portRange containertypes.PortRange
err error
)
// Empty HostPort means to map to an ephemeral port.
if binding.HostPort != "" {
portRange, err = containertypes.ParsePortRange(binding.HostPort)
if err != nil {
return nil, fmt.Errorf("error parsing HostPort value(%s):%v", binding.HostPort, err)
}
}
publishedPorts = append(publishedPorts, lntypes.PortBinding{
Proto: portProto,
Port: portNum,
Proto: protocol,
Port: p.Num(),
HostIP: net.ParseIP(binding.HostIP),
HostPort: uint16(portStart),
HostPortEnd: uint16(portEnd),
HostPort: portRange.Start(),
HostPortEnd: portRange.End(),
})
}
if c.HostConfig.PublishAllPorts && len(bindings[port]) == 0 {
if c.HostConfig.PublishAllPorts && len(bindings) == 0 {
publishedPorts = append(publishedPorts, lntypes.PortBinding{
Proto: portProto,
Port: portNum,
Proto: protocol,
Port: p.Num(),
})
}
}
@@ -1038,9 +1028,9 @@ func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint)
if expData, ok := driverInfo[netlabel.ExposedPorts]; ok {
if exposedPorts, ok := expData.([]lntypes.TransportPort); ok {
for _, tp := range exposedPorts {
natPort, err := nat.NewPort(tp.Proto.String(), strconv.Itoa(int(tp.Port)))
if err != nil {
log.G(context.TODO()).Errorf("invalid exposed port %s: %v", tp.String(), err)
natPort, ok := containertypes.PortFrom(tp.Port, containertypes.NetworkProtocol(tp.Proto.String()))
if !ok {
log.G(context.TODO()).Errorf("Invalid exposed port: %s", tp.String())
continue
}
if _, ok := pm[natPort]; !ok {
@@ -1057,12 +1047,13 @@ func getEndpointPortMapInfo(pm containertypes.PortMap, ep *libnetwork.Endpoint)
if portMapping, ok := mapData.([]lntypes.PortBinding); ok {
for _, pp := range portMapping {
// Use an empty string for the host port if there's no port assigned.
natPort, err := nat.NewPort(pp.Proto.String(), strconv.Itoa(int(pp.Port)))
if err != nil {
log.G(context.TODO()).Errorf("invalid port binding %s: %v", pp, err)
// Use an empty string for the host natPort if there's no natPort assigned.
natPort, ok := containertypes.PortFrom(pp.Port, containertypes.NetworkProtocol(pp.Proto.String()))
if !ok {
log.G(context.TODO()).Errorf("Invalid port binding: %s", pp.String())
continue
}
var hp string
if pp.HostPort > 0 {
hp = strconv.Itoa(int(pp.HostPort))

View File

@@ -12,7 +12,6 @@ import (
"github.com/containerd/log"
"github.com/containerd/platforms"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/filters"
@@ -891,7 +890,7 @@ func handleSysctlBC(
func handlePortBindingsBC(hostConfig *container.HostConfig, version string) string {
var emptyPBs []string
for portProto, bindings := range hostConfig.PortBindings {
for port, bindings := range hostConfig.PortBindings {
if len(bindings) > 0 {
continue
}
@@ -902,15 +901,15 @@ func handlePortBindingsBC(hostConfig *container.HostConfig, version string) stri
// on-disk state for containers created by older versions of the
// Engine. Drop the PortBindings entry to ensure that no backfilling
// will happen when restarting the daemon.
delete(hostConfig.PortBindings, portProto)
delete(hostConfig.PortBindings, port)
continue
}
if versions.Equal(version, "1.52") {
emptyPBs = append(emptyPBs, string(portProto))
emptyPBs = append(emptyPBs, port.String())
}
hostConfig.PortBindings[portProto] = []nat.PortBinding{{}}
hostConfig.PortBindings[port] = []container.PortBinding{{}}
}
if len(emptyPBs) > 0 {

View File

@@ -505,7 +505,7 @@ func (s *DockerAPISuite) TestContainerAPIBadPort(c *testing.T) {
hostConfig := container.HostConfig{
PortBindings: container.PortMap{
"8080/tcp": []container.PortBinding{
container.MustParsePort("8080/tcp"): []container.PortBinding{
{
HostIP: "",
HostPort: "aa80",

View File

@@ -92,7 +92,7 @@ func (s *DockerCLICreateSuite) TestCreateWithPortRange(c *testing.T) {
var containers []struct {
HostConfig *struct {
PortBindings map[containertypes.PortRangeProto][]containertypes.PortBinding
PortBindings map[containertypes.Port][]containertypes.PortBinding
}
}
err := json.Unmarshal([]byte(out), &containers)
@@ -105,8 +105,8 @@ func (s *DockerCLICreateSuite) TestCreateWithPortRange(c *testing.T) {
assert.Equal(c, len(cont.HostConfig.PortBindings), 4, fmt.Sprintf("Expected 4 ports bindings, got %d", len(cont.HostConfig.PortBindings)))
for k, v := range cont.HostConfig.PortBindings {
assert.Equal(c, len(v), 1, fmt.Sprintf("Expected 1 ports binding, for the port %s but found %s", k, v))
assert.Equal(c, k.Port(), v[0].HostPort, fmt.Sprintf("Expected host port %s to match published port %s", k.Port(), v[0].HostPort))
assert.Equal(c, len(v), 1, fmt.Sprintf("Expected 1 ports binding, for the port %s but found %s", k, v))
assert.Equal(c, fmt.Sprintf("%d", k.Num()), v[0].HostPort, fmt.Sprintf("Expected host port %d to match published port %s", k.Num(), v[0].HostPort))
}
}
@@ -118,7 +118,7 @@ func (s *DockerCLICreateSuite) TestCreateWithLargePortRange(c *testing.T) {
var containers []struct {
HostConfig *struct {
PortBindings map[containertypes.PortRangeProto][]containertypes.PortBinding
PortBindings map[containertypes.Port][]containertypes.PortBinding
}
}
@@ -132,7 +132,7 @@ func (s *DockerCLICreateSuite) TestCreateWithLargePortRange(c *testing.T) {
for k, v := range cont.HostConfig.PortBindings {
assert.Equal(c, len(v), 1)
assert.Equal(c, k.Port(), v[0].HostPort, fmt.Sprintf("Expected host port %s to match published port %s", k.Port(), v[0].HostPort))
assert.Equal(c, fmt.Sprintf("%d", k.Num()), v[0].HostPort, fmt.Sprintf("Expected host port %d to match published port %s", k.Num(), v[0].HostPort))
}
}

View File

@@ -197,11 +197,11 @@ func assertPortRange(ctx context.Context, id string, expectedTCP, expectedUDP []
}
var validTCP, validUDP bool
for portAndProto, binding := range inspect.NetworkSettings.Ports {
if portAndProto.Proto() == "tcp" && len(expectedTCP) == 0 {
for port, binding := range inspect.NetworkSettings.Ports {
if port.Proto() == "tcp" && len(expectedTCP) == 0 {
continue
}
if portAndProto.Proto() == "udp" && len(expectedTCP) == 0 {
if port.Proto() == "udp" && len(expectedTCP) == 0 {
continue
}

View File

@@ -2173,9 +2173,8 @@ func (s *DockerCLIRunSuite) TestRunAllowPortRangeThroughExpose(c *testing.T) {
c.Fatal(err)
}
for port, binding := range ports {
portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0])
if portnum < 3000 || portnum > 3003 {
c.Fatalf("Port %d is out of range ", portnum)
if port.Num() < 3000 || port.Num() > 3003 {
c.Fatalf("Port %d is out of range", port.Num())
}
if len(binding) == 0 || binding[0].HostPort == "" {
c.Fatalf("Port is not mapped for the port %s", port)
@@ -2510,12 +2509,11 @@ func (s *DockerCLIRunSuite) TestRunAllowPortRangeThroughPublish(c *testing.T) {
err := json.Unmarshal([]byte(portStr), &ports)
assert.NilError(c, err, "failed to unmarshal: %v", portStr)
for port, binding := range ports {
portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0])
if portnum < 3000 || portnum > 3003 {
c.Fatalf("Port %d is out of range ", portnum)
if port.Num() < 3000 || port.Num() > 3003 {
c.Fatalf("Port %d is out of range", port.Num())
}
if len(binding) == 0 || binding[0].HostPort == "" {
c.Fatal("Port is not mapped for the port "+port, id)
c.Fatalf("Port is not mapped for the port %s", port)
}
}
}

View File

@@ -71,12 +71,13 @@ func TestNetworkStateCleanupOnDaemonStart(t *testing.T) {
defer d.Stop(t)
apiClient := d.NewClientT(t)
mappedPort := containertypes.MustParsePort("80/tcp")
// The intention of this container is to ignore stop signals.
// Sadly this means the test will take longer, but at least this test can be parallelized.
cid := container.Run(ctx, t, apiClient,
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {{}}}),
container.WithPortMap(containertypes.PortMap{mappedPort: {{}}}),
container.WithCmd("/bin/sh", "-c", "while true; do echo hello; sleep 1; done"))
defer func() {
err := apiClient.ContainerRemove(ctx, cid, client.ContainerRemoveOptions{Force: true})
@@ -87,7 +88,7 @@ func TestNetworkStateCleanupOnDaemonStart(t *testing.T) {
assert.NilError(t, err)
assert.Assert(t, inspect.NetworkSettings.SandboxID != "")
assert.Assert(t, inspect.NetworkSettings.SandboxKey != "")
assert.Assert(t, inspect.NetworkSettings.Ports["80/tcp"] != nil)
assert.Assert(t, inspect.NetworkSettings.Ports[mappedPort] != nil)
assert.NilError(t, d.Kill())
d.Start(t)
@@ -96,5 +97,5 @@ func TestNetworkStateCleanupOnDaemonStart(t *testing.T) {
assert.NilError(t, err)
assert.Assert(t, inspect.NetworkSettings.SandboxID == "")
assert.Assert(t, inspect.NetworkSettings.SandboxKey == "")
assert.Assert(t, is.Nil(inspect.NetworkSettings.Ports["80/tcp"]))
assert.Assert(t, is.Nil(inspect.NetworkSettings.Ports[mappedPort]))
}

View File

@@ -113,7 +113,7 @@ func TestNetworkLoopbackNat(t *testing.T) {
assert.Check(t, is.Equal(msg, strings.TrimSpace(b.String())))
}
func startServerContainer(ctx context.Context, t *testing.T, msg string, port int) string {
func startServerContainer(ctx context.Context, t *testing.T, msg string, port uint16) string {
t.Helper()
apiClient := testEnv.APIClient()
@@ -123,7 +123,7 @@ func startServerContainer(ctx context.Context, t *testing.T, msg string, port in
container.WithExposedPorts(fmt.Sprintf("%d/tcp", port)),
func(c *container.TestContainerConfig) {
c.HostConfig.PortBindings = containertypes.PortMap{
containertypes.PortRangeProto(fmt.Sprintf("%d/tcp", port)): []containertypes.PortBinding{
containertypes.MustParsePort(fmt.Sprintf("%d/tcp", port)): []containertypes.PortBinding{
{
HostPort: fmt.Sprintf("%d", port),
},

View File

@@ -73,9 +73,10 @@ func WithSysctls(sysctls map[string]string) func(*TestContainerConfig) {
// WithExposedPorts sets the exposed ports of the container
func WithExposedPorts(ports ...string) func(*TestContainerConfig) {
return func(c *TestContainerConfig) {
c.Config.ExposedPorts = map[container.PortRangeProto]struct{}{}
c.Config.ExposedPorts = map[container.Port]struct{}{}
for _, port := range ports {
c.Config.ExposedPorts[container.PortRangeProto(port)] = struct{}{}
p, _ := container.ParsePort(port)
c.Config.ExposedPorts[p] = struct{}{}
}
}
}

View File

@@ -10,7 +10,6 @@ import (
"testing"
"time"
"github.com/docker/go-connections/nat"
containertypes "github.com/moby/moby/api/types/container"
networktypes "github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/versions"
@@ -518,18 +517,19 @@ func TestEndpointWithCustomIfname(t *testing.T) {
func TestPublishedPortAlreadyInUse(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
mappedPort := containertypes.MustParsePort("80/tcp")
ctr1 := ctr.Run(ctx, t, apiClient,
ctr.WithCmd("top"),
ctr.WithExposedPorts("80/tcp"),
ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}}))
ctr.WithPortMap(containertypes.PortMap{mappedPort: {{HostPort: "8000"}}}))
defer ctr.Remove(ctx, t, apiClient, ctr1, client.ContainerRemoveOptions{Force: true})
ctr2 := ctr.Create(ctx, t, apiClient,
ctr.WithCmd("top"),
ctr.WithRestartPolicy(containertypes.RestartPolicyAlways),
ctr.WithExposedPorts("80/tcp"),
ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}}))
ctr.WithPortMap(containertypes.PortMap{mappedPort: {{HostPort: "8000"}}}))
defer ctr.Remove(ctx, t, apiClient, ctr2, client.ContainerRemoveOptions{Force: true})
err := apiClient.ContainerStart(ctx, ctr2, client.ContainerStartOptions{})
@@ -564,18 +564,18 @@ func TestAllPortMappingsAreReturned(t *testing.T) {
ctrID := ctr.Run(ctx, t, apiClient,
ctr.WithExposedPorts("80/tcp", "81/tcp"),
ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}}),
ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}}),
ctr.WithEndpointSettings("testnetv4", &networktypes.EndpointSettings{}),
ctr.WithEndpointSettings("testnetv6", &networktypes.EndpointSettings{}))
defer ctr.Remove(ctx, t, apiClient, ctrID, client.ContainerRemoveOptions{Force: true})
inspect := ctr.Inspect(ctx, t, apiClient, ctrID)
assert.DeepEqual(t, inspect.NetworkSettings.Ports, containertypes.PortMap{
"80/tcp": []containertypes.PortBinding{
containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
{HostIP: "0.0.0.0", HostPort: "8000"},
{HostIP: "::", HostPort: "8000"},
},
"81/tcp": nil,
containertypes.MustParsePort("81/tcp"): nil,
})
}
@@ -603,7 +603,7 @@ func TestFirewalldReloadNoZombies(t *testing.T) {
cid := ctr.Run(ctx, t, c,
ctr.WithExposedPorts("80/tcp", "81/tcp"),
ctr.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8000"}}}))
ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8000"}}}))
defer func() {
if !removed {
ctr.Remove(ctx, t, c, cid, client.ContainerRemoveOptions{Force: true})
@@ -793,7 +793,7 @@ func TestPortMappingRestore(t *testing.T) {
const svrName = "svr"
cid := ctr.Run(ctx, t, c,
ctr.WithExposedPorts("80/tcp"),
ctr.WithPortMap(containertypes.PortMap{"80/tcp": {}}),
ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}),
ctr.WithName(svrName),
ctr.WithRestartPolicy(containertypes.RestartPolicyUnlessStopped),
ctr.WithCmd("httpd", "-f"),
@@ -804,9 +804,9 @@ func TestPortMappingRestore(t *testing.T) {
t.Helper()
insp := ctr.Inspect(ctx, t, c, cid)
assert.Check(t, is.Equal(insp.State.Running, true))
if assert.Check(t, is.Contains(insp.NetworkSettings.Ports, containertypes.PortRangeProto("80/tcp"))) &&
assert.Check(t, is.Len(insp.NetworkSettings.Ports["80/tcp"], 2)) {
hostPort := insp.NetworkSettings.Ports["80/tcp"][0].HostPort
if assert.Check(t, is.Contains(insp.NetworkSettings.Ports, containertypes.MustParsePort("80/tcp"))) &&
assert.Check(t, is.Len(insp.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")], 2)) {
hostPort := insp.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
res := ctr.RunAttach(ctx, t, c,
ctr.WithExtraHost("thehost:host-gateway"),
ctr.WithCmd("wget", "-T3", "http://"+net.JoinHostPort("thehost", hostPort)),
@@ -951,7 +951,7 @@ func TestEmptyPortBindingsBC(t *testing.T) {
d.StartWithBusybox(ctx, t)
defer d.Stop(t)
createInspect := func(t *testing.T, version string, pbs []nat.PortBinding) (containertypes.PortMap, []string) {
createInspect := func(t *testing.T, version string, pbs []containertypes.PortBinding) (containertypes.PortMap, []string) {
apiClient := d.NewClientT(t, client.WithVersion(version))
defer apiClient.Close()
@@ -966,7 +966,7 @@ func TestEmptyPortBindingsBC(t *testing.T) {
// Create a container with an empty list of port bindings for container port 80/tcp.
config := ctr.NewTestConfig(ctr.WithCmd("top"),
ctr.WithExposedPorts("80/tcp"),
ctr.WithPortMap(containertypes.PortMap{"80/tcp": pbs}))
ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): pbs}))
c, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
assert.NilError(t, err)
defer apiClient.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{Force: true})
@@ -979,25 +979,25 @@ func TestEmptyPortBindingsBC(t *testing.T) {
}
t.Run("backfilling on old client version", func(t *testing.T) {
expMappings := containertypes.PortMap{"80/tcp": {
expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
{}, // An empty PortBinding is backfilled
}}
expWarnings := make([]string, 0)
mappings, warnings := createInspect(t, "1.51", []nat.PortBinding{})
mappings, warnings := createInspect(t, "1.51", []containertypes.PortBinding{})
assert.DeepEqual(t, expMappings, mappings)
assert.DeepEqual(t, expWarnings, warnings)
})
t.Run("backfilling on API 1.52, with a warning", func(t *testing.T) {
expMappings := containertypes.PortMap{"80/tcp": {
expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
{}, // An empty PortBinding is backfilled
}}
expWarnings := []string{
"Following container port(s) have an empty list of port-bindings: 80/tcp. Starting with API 1.53, such bindings will be discarded.",
}
mappings, warnings := createInspect(t, "1.52", []nat.PortBinding{})
mappings, warnings := createInspect(t, "1.52", []containertypes.PortBinding{})
assert.DeepEqual(t, expMappings, mappings)
assert.DeepEqual(t, expWarnings, warnings)
})
@@ -1006,19 +1006,19 @@ func TestEmptyPortBindingsBC(t *testing.T) {
expMappings := containertypes.PortMap{}
expWarnings := make([]string, 0)
mappings, warnings := createInspect(t, "1.53", []nat.PortBinding{})
mappings, warnings := createInspect(t, "1.53", []containertypes.PortBinding{})
assert.DeepEqual(t, expMappings, mappings)
assert.DeepEqual(t, expWarnings, warnings)
})
for _, apiVersion := range []string{"1.51", "1.52", "1.53"} {
t.Run("no backfilling on API "+apiVersion+" with non-empty bindings", func(t *testing.T) {
expMappings := containertypes.PortMap{"80/tcp": {
expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
{HostPort: "8080"},
}}
expWarnings := make([]string, 0)
mappings, warnings := createInspect(t, apiVersion, []nat.PortBinding{{HostPort: "8080"}})
mappings, warnings := createInspect(t, apiVersion, []containertypes.PortBinding{{HostPort: "8080"}})
assert.DeepEqual(t, expMappings, mappings)
assert.DeepEqual(t, expWarnings, warnings)
})
@@ -1043,7 +1043,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) {
cid := ctr.Create(ctx, t, c,
ctr.WithExposedPorts("80/tcp"),
ctr.WithPortMap(containertypes.PortMap{"80/tcp": {}}))
ctr.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}))
defer c.ContainerRemove(ctx, cid, client.ContainerRemoveOptions{Force: true})
// Stop the daemon to safely tamper with the on-disk state.
@@ -1052,7 +1052,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) {
d.TamperWithContainerConfig(t, cid, func(container *container.Container) {
// Simulate a container created with an older version of the Engine
// by setting an empty list of port bindings.
container.HostConfig.PortBindings = containertypes.PortMap{"80/tcp": {}}
container.HostConfig.PortBindings = containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}
})
// Restart the daemon — it should backfill the empty port binding slice.
@@ -1060,7 +1060,7 @@ func TestPortBindingBackfillingForOlderContainers(t *testing.T) {
inspect := ctr.Inspect(ctx, t, c, cid)
expMappings := containertypes.PortMap{"80/tcp": {
expMappings := containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
{}, // An empty PortBinding is backfilled
}}
assert.DeepEqual(t, expMappings, inspect.HostConfig.PortBindings)

View File

@@ -82,7 +82,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -95,7 +95,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -108,7 +108,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -142,7 +142,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -155,7 +155,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -167,7 +167,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -179,7 +179,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostIP: "127.0.0.1", HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: "127.0.0.1", HostPort: "8080"}}},
},
},
}},
@@ -327,7 +327,7 @@ func createBridgeNetworks(ctx context.Context, t *testing.T, d *daemon.Daemon, s
for _, ctr := range nw.containers {
var exposedPorts []string
for ep := range ctr.portMappings {
exposedPorts = append(exposedPorts, ep.Port()+"/"+ep.Proto())
exposedPorts = append(exposedPorts, ep.String())
}
id := container.Run(ctx, t, c,
container.WithNetworkMode(nw.name),
@@ -356,7 +356,7 @@ func createServices(ctx context.Context, t *testing.T, d *daemon.Daemon, section
portConfig = append(portConfig, swarmtypes.PortConfig{
Protocol: swarmtypes.PortConfigProtocol(ctrPP.Proto()),
PublishedPort: uint32(hp),
TargetPort: uint32(ctrPP.Int()),
TargetPort: uint32(ctrPP.Num()),
})
}
}

View File

@@ -79,7 +79,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -92,7 +92,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -105,7 +105,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -139,7 +139,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -152,7 +152,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}},
},
},
}},
@@ -178,7 +178,7 @@ var index = []section{
containers: []ctrDesc{
{
name: "c1",
portMappings: containertypes.PortMap{"80/tcp": {{HostIP: "127.0.0.1", HostPort: "8080"}}},
portMappings: containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: "127.0.0.1", HostPort: "8080"}}},
},
},
}},
@@ -300,7 +300,7 @@ func createBridgeNetworks(ctx context.Context, t *testing.T, d *daemon.Daemon, s
for _, ctr := range nw.containers {
var exposedPorts []string
for ep := range ctr.portMappings {
exposedPorts = append(exposedPorts, ep.Port()+"/"+ep.Proto())
exposedPorts = append(exposedPorts, ep.String())
}
id := container.Run(ctx, t, c,
container.WithNetworkMode(nw.name),

View File

@@ -388,7 +388,7 @@ func TestBridgeINCRouted(t *testing.T) {
container.WithNetworkMode(netName),
container.WithName("ctr-"+gwMode),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {}}),
)
t.Cleanup(func() {
c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
@@ -565,7 +565,7 @@ func TestAccessToPublishedPort(t *testing.T) {
container.WithNetworkMode(serverNetName),
container.WithName("ctr-server"),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {containertypes.PortBinding{HostPort: "8080"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {containertypes.PortBinding{HostPort: "8080"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
@@ -688,7 +688,7 @@ func TestInterNetworkDirectRouting(t *testing.T) {
container.WithNetworkMode(serverNetName),
container.WithName("ctr-pub"),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {containertypes.PortBinding{HostPort: "8080"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {containertypes.PortBinding{HostPort: "8080"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, ctrPubId, client.ContainerRemoveOptions{Force: true})
@@ -1063,7 +1063,7 @@ func TestDisableIPv6OnInterface(t *testing.T) {
container.WithName(ctrName),
container.WithNetworkMode(tc.netName),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}),
container.WithEndpointSettings(tc.netName, &networktypes.EndpointSettings{
DriverOpts: map[string]string{
netlabel.EndpointSysctls: "net.ipv6.conf.IFNAME.disable_ipv6=1",
@@ -1504,7 +1504,7 @@ func TestGatewaySelection(t *testing.T) {
container.WithName(ctrName),
container.WithNetworkMode(netName4),
container.WithExposedPorts("80"),
container.WithPortMap(containertypes.PortMap{"80": {{HostPort: "8080"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostPort: "8080"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
@@ -1956,7 +1956,7 @@ func TestDropInForwardChain(t *testing.T) {
ctrId := container.Run(ctx, t, c,
container.WithNetworkMode(netName46),
container.WithExposedPorts("80"),
container.WithPortMap(containertypes.PortMap{"80": {{HostPort: hostPort}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostPort: hostPort}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})

View File

@@ -97,13 +97,13 @@ func TestFlakyPortMappedHairpinWindows(t *testing.T) {
serverId := container.Run(ctx, t, c,
container.WithNetworkMode(serverNetName),
container.WithExposedPorts("80"),
container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "0.0.0.0"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: "0.0.0.0"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
inspect := container.Inspect(ctx, t, c, serverId)
hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort
hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
attachCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()

View File

@@ -73,7 +73,7 @@ func TestDisableNAT(t *testing.T) {
{
name: "defaults",
expPortMap: containertypes.PortMap{
"80/tcp": []containertypes.PortBinding{
containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
{HostIP: "0.0.0.0", HostPort: "8080"},
{HostIP: "::", HostPort: "8080"},
},
@@ -84,7 +84,7 @@ func TestDisableNAT(t *testing.T) {
gwMode4: "nat",
gwMode6: "routed",
expPortMap: containertypes.PortMap{
"80/tcp": []containertypes.PortBinding{
containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
{HostIP: "0.0.0.0", HostPort: "8080"},
{HostIP: "::", HostPort: ""},
},
@@ -95,7 +95,7 @@ func TestDisableNAT(t *testing.T) {
gwMode4: "routed",
gwMode6: "nat",
expPortMap: containertypes.PortMap{
"80/tcp": []containertypes.PortBinding{
containertypes.MustParsePort("80/tcp"): []containertypes.PortBinding{
{HostIP: "::", HostPort: "8080"},
{HostIP: "0.0.0.0", HostPort: ""},
},
@@ -124,7 +124,7 @@ func TestDisableNAT(t *testing.T) {
id := container.Run(ctx, t, c,
container.WithNetworkMode(netName),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}}}),
)
defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true})
@@ -162,13 +162,13 @@ func TestPortMappedHairpinTCP(t *testing.T) {
serverId := container.Run(ctx, t, c,
container.WithNetworkMode(serverNetName),
container.WithExposedPorts("80"),
container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "0.0.0.0"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: "0.0.0.0"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
inspect := container.Inspect(ctx, t, c, serverId)
hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort
hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
clientCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
@@ -209,13 +209,13 @@ func TestPortMappedHairpinUDP(t *testing.T) {
serverId := container.Run(ctx, t, c,
container.WithNetworkMode(serverNetName),
container.WithExposedPorts("54/udp"),
container.WithPortMap(containertypes.PortMap{"54/udp": {{HostIP: "0.0.0.0"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("54/udp"): {{HostIP: "0.0.0.0"}}}),
container.WithCmd("/bin/sh", "-c", "echo 'foobar.internal 192.168.155.23' | dnsd -c - -p 54"),
)
defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
inspect := container.Inspect(ctx, t, c, serverId)
hostPort := inspect.NetworkSettings.Ports["54/udp"][0].HostPort
hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("54/udp")][0].HostPort
// nslookup gets an answer quickly from the dns server, but then tries to
// query another DNS server (for some unknown reasons) and times out. Hence,
@@ -251,13 +251,13 @@ func TestProxy4To6(t *testing.T) {
serverId := container.Run(ctx, t, c,
container.WithNetworkMode(netName),
container.WithExposedPorts("80"),
container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "::1"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80"): {{HostIP: "::1"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
inspect := container.Inspect(ctx, t, c, serverId)
hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort
hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
var resp *http.Response
addr := "http://[::1]:" + hostPort
@@ -375,7 +375,7 @@ func TestAccessPublishedPortFromHost(t *testing.T) {
serverID := container.Run(ctx, t, c,
container.WithName(sanitizeCtrName(t.Name()+"-server")),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: hostPort}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}),
container.WithCmd("httpd", "-f"),
container.WithNetworkMode(bridgeName))
defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
@@ -455,7 +455,7 @@ func TestAccessPublishedPortFromRemoteHost(t *testing.T) {
serverID := container.Run(ctx, t, c,
container.WithName(sanitizeCtrName(t.Name()+"-server")),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: hostPort}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostPort: hostPort}}}),
container.WithCmd("httpd", "-f"),
container.WithNetworkMode(bridgeName))
defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
@@ -553,13 +553,13 @@ func TestAccessPublishedPortFromCtr(t *testing.T) {
serverId := container.Run(ctx, t, c,
container.WithNetworkMode(netName),
container.WithExposedPorts("80"),
container.WithPortMap(containertypes.PortMap{"80": {{HostIP: "0.0.0.0"}}}),
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {{HostIP: "0.0.0.0"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, serverId, client.ContainerRemoveOptions{Force: true})
inspect := container.Inspect(ctx, t, c, serverId)
hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort
hostPort := inspect.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")][0].HostPort
clientCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
@@ -603,7 +603,9 @@ func TestRestartUserlandProxyUnder2MSL(t *testing.T) {
ctrOpts := []func(*container.TestContainerConfig){
container.WithName(ctrName),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "1780"}}}),
container.WithPortMap(containertypes.PortMap{
containertypes.MustParsePort("80/tcp"): {{HostPort: "1780"}},
}),
container.WithCmd("httpd", "-f"),
container.WithNetworkMode(netName),
}
@@ -700,7 +702,9 @@ func TestDirectRoutingOpenPorts(t *testing.T) {
container.WithNetworkMode(netName),
container.WithName("ctr-"+gwMode),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {}}),
container.WithPortMap(containertypes.PortMap{
containertypes.MustParsePort("80/tcp"): {},
}),
)
t.Cleanup(func() {
c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})
@@ -979,7 +983,9 @@ func TestRoutedNonGateway(t *testing.T) {
ctrId := container.Run(ctx, t, c,
container.WithCmd("httpd", "-f"),
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {{HostPort: "8080"}}}),
container.WithPortMap(containertypes.PortMap{
containertypes.MustParsePort("80/tcp"): {{HostPort: "8080"}},
}),
container.WithNetworkMode(natNetName),
container.WithNetworkMode(routedNetName),
container.WithEndpointSettings(natNetName, &networktypes.EndpointSettings{GwPriority: 1}),
@@ -1133,7 +1139,9 @@ func TestAccessPublishedPortFromAnotherNetwork(t *testing.T) {
container.WithName("server"),
container.WithCmd("nc", "-lp", "5000"),
container.WithExposedPorts("5000/tcp"),
container.WithPortMap(containertypes.PortMap{"5000/tcp": {{HostPort: "5000"}}}),
container.WithPortMap(containertypes.PortMap{
containertypes.MustParsePort("5000/tcp"): {{HostPort: "5000"}},
}),
container.WithNetworkMode(servnet))
defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
@@ -1329,7 +1337,9 @@ func testDirectRemoteAccessOnExposedPort(t *testing.T, ctx context.Context, d *d
container.WithName(sanitizeCtrName(t.Name()+"-server")),
container.WithCmd("nc", "-lup", "5000"),
container.WithExposedPorts("5000/udp"),
container.WithPortMap(containertypes.PortMap{"5000/udp": {{HostPort: hostPort}}}),
container.WithPortMap(containertypes.PortMap{
containertypes.MustParsePort("5000/udp"): {{HostPort: hostPort}},
}),
container.WithNetworkMode(bridgeName),
container.WithEndpointSettings(bridgeName, &networktypes.EndpointSettings{
IPAddress: ctrIP.String(),
@@ -1415,7 +1425,9 @@ func TestAccessPortPublishedOnLoopbackAddress(t *testing.T) {
container.WithCmd("nc", "-lup", "5000"),
container.WithExposedPorts("5000/udp"),
// This port is mapped on 127.0.0.2, so it should not be remotely accessible.
container.WithPortMap(containertypes.PortMap{"5000/udp": {{HostIP: loIP, HostPort: hostPort}}}),
container.WithPortMap(containertypes.PortMap{
containertypes.MustParsePort("5000/udp"): {{HostIP: loIP, HostPort: hostPort}},
}),
container.WithNetworkMode(bridgeName))
defer c.ContainerRemove(ctx, serverID, client.ContainerRemoveOptions{Force: true})
@@ -1526,7 +1538,7 @@ func TestSkipRawRules(t *testing.T) {
ctrId := container.Run(ctx, t, c,
container.WithExposedPorts("80/tcp"),
container.WithPortMap(containertypes.PortMap{"80/tcp": {
container.WithPortMap(containertypes.PortMap{containertypes.MustParsePort("80/tcp"): {
{HostIP: "127.0.0.1", HostPort: "8080"},
{HostPort: "8081"},
}}),
@@ -1559,9 +1571,9 @@ func TestMixAnyWithSpecificHostAddrs(t *testing.T) {
ctrId := container.Run(ctx, t, c,
container.WithExposedPorts("80/"+proto, "81/"+proto, "82/"+proto),
container.WithPortMap(containertypes.PortMap{
containertypes.PortRangeProto("81/" + proto): {{}},
containertypes.PortRangeProto("82/" + proto): {{}},
containertypes.PortRangeProto("80/" + proto): {{HostIP: "127.0.0.1"}},
containertypes.MustParsePort("81/" + proto): {{}},
containertypes.MustParsePort("82/" + proto): {{}},
containertypes.MustParsePort("80/" + proto): {{HostIP: "127.0.0.1"}},
}),
)
defer c.ContainerRemove(ctx, ctrId, client.ContainerRemoveOptions{Force: true})

View File

@@ -164,7 +164,7 @@ COPY . /static`); err != nil {
// Find out the system assigned port
i, err := c.ContainerInspect(context.Background(), b.ID)
assert.NilError(t, err)
ports, exists := i.NetworkSettings.Ports["80/tcp"]
ports, exists := i.NetworkSettings.Ports[containertypes.MustParsePort("80/tcp")]
assert.Assert(t, exists, "unable to find port 80/tcp for %s", ctrName)
if len(ports) == 0 {
t.Fatalf("no ports mapped for 80/tcp for %s: %#v", ctrName, i.NetworkSettings.Ports)

View File

@@ -1,237 +0,0 @@
// Package nat is a convenience package for manipulation of strings describing network ports.
package nat
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
)
// PortBinding represents a binding between a Host IP address and a Host Port
type PortBinding struct {
// HostIP is the host IP Address
HostIP string `json:"HostIp"`
// HostPort is the host port number
HostPort string
}
// PortMap is a collection of PortBinding indexed by Port
type PortMap map[Port][]PortBinding
// PortSet is a collection of structs indexed by Port
type PortSet map[Port]struct{}
// Port is a string containing port number and protocol in the format "80/tcp"
type Port string
// NewPort creates a new instance of a Port given a protocol and port number or port range
func NewPort(proto, port string) (Port, error) {
// Check for parsing issues on "port" now so we can avoid having
// to check it later on.
portStartInt, portEndInt, err := ParsePortRangeToInt(port)
if err != nil {
return "", err
}
if portStartInt == portEndInt {
return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil
}
return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil
}
// ParsePort parses the port number string and returns an int
func ParsePort(rawPort string) (int, error) {
if rawPort == "" {
return 0, nil
}
port, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
return 0, fmt.Errorf("invalid port '%s': %w", rawPort, errors.Unwrap(err))
}
return int(port), nil
}
// ParsePortRangeToInt parses the port range string and returns start/end ints
func ParsePortRangeToInt(rawPort string) (int, int, error) {
if rawPort == "" {
return 0, 0, nil
}
start, end, err := ParsePortRange(rawPort)
if err != nil {
return 0, 0, err
}
return int(start), int(end), nil
}
// Proto returns the protocol of a Port
func (p Port) Proto() string {
proto, _ := SplitProtoPort(string(p))
return proto
}
// Port returns the port number of a Port
func (p Port) Port() string {
_, port := SplitProtoPort(string(p))
return port
}
// Int returns the port number of a Port as an int
func (p Port) Int() int {
portStr := p.Port()
// We don't need to check for an error because we're going to
// assume that any error would have been found, and reported, in NewPort()
port, _ := ParsePort(portStr)
return port
}
// Range returns the start/end port numbers of a Port range as ints
func (p Port) Range() (int, int, error) {
return ParsePortRangeToInt(p.Port())
}
// SplitProtoPort splits a port(range) and protocol, formatted as "<portnum>/[<proto>]"
// "<startport-endport>/[<proto>]". It returns an empty string for both if
// no port(range) is provided. If a port(range) is provided, but no protocol,
// the default ("tcp") protocol is returned.
//
// SplitProtoPort does not validate or normalize the returned values.
func SplitProtoPort(rawPort string) (proto string, port string) {
port, proto, _ = strings.Cut(rawPort, "/")
if port == "" {
return "", ""
}
if proto == "" {
proto = "tcp"
}
return proto, port
}
func validateProto(proto string) error {
switch proto {
case "tcp", "udp", "sctp":
// All good
return nil
default:
return errors.New("invalid proto: " + proto)
}
}
// ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses
// these in to the internal types
func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error) {
var (
exposedPorts = make(map[Port]struct{}, len(ports))
bindings = make(map[Port][]PortBinding)
)
for _, p := range ports {
portMappings, err := ParsePortSpec(p)
if err != nil {
return nil, nil, err
}
for _, pm := range portMappings {
port := pm.Port
if _, ok := exposedPorts[port]; !ok {
exposedPorts[port] = struct{}{}
}
bindings[port] = append(bindings[port], pm.Binding)
}
}
return exposedPorts, bindings, nil
}
// PortMapping is a data object mapping a Port to a PortBinding
type PortMapping struct {
Port Port
Binding PortBinding
}
func (p *PortMapping) String() string {
return net.JoinHostPort(p.Binding.HostIP, p.Binding.HostPort+":"+string(p.Port))
}
func splitParts(rawport string) (hostIP, hostPort, containerPort string) {
parts := strings.Split(rawport, ":")
switch len(parts) {
case 1:
return "", "", parts[0]
case 2:
return "", parts[0], parts[1]
case 3:
return parts[0], parts[1], parts[2]
default:
n := len(parts)
return strings.Join(parts[:n-2], ":"), parts[n-2], parts[n-1]
}
}
// ParsePortSpec parses a port specification string into a slice of PortMappings
func ParsePortSpec(rawPort string) ([]PortMapping, error) {
ip, hostPort, containerPort := splitParts(rawPort)
proto, containerPort := SplitProtoPort(containerPort)
proto = strings.ToLower(proto)
if err := validateProto(proto); err != nil {
return nil, err
}
if ip != "" && ip[0] == '[' {
// Strip [] from IPV6 addresses
rawIP, _, err := net.SplitHostPort(ip + ":")
if err != nil {
return nil, fmt.Errorf("invalid IP address %v: %w", ip, err)
}
ip = rawIP
}
if ip != "" && net.ParseIP(ip) == nil {
return nil, errors.New("invalid IP address: " + ip)
}
if containerPort == "" {
return nil, fmt.Errorf("no port specified: %s<empty>", rawPort)
}
startPort, endPort, err := ParsePortRange(containerPort)
if err != nil {
return nil, errors.New("invalid containerPort: " + containerPort)
}
var startHostPort, endHostPort uint64
if hostPort != "" {
startHostPort, endHostPort, err = ParsePortRange(hostPort)
if err != nil {
return nil, errors.New("invalid hostPort: " + hostPort)
}
if (endPort - startPort) != (endHostPort - startHostPort) {
// Allow host port range iff containerPort is not a range.
// In this case, use the host port range as the dynamic
// host port range to allocate into.
if endPort != startPort {
return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
}
}
}
count := endPort - startPort + 1
ports := make([]PortMapping, 0, count)
for i := uint64(0); i < count; i++ {
cPort := Port(strconv.FormatUint(startPort+i, 10) + "/" + proto)
hPort := ""
if hostPort != "" {
hPort = strconv.FormatUint(startHostPort+i, 10)
// Set hostPort to a range only if there is a single container port
// and a dynamic host port.
if count == 1 && startHostPort != endHostPort {
hPort += "-" + strconv.FormatUint(endHostPort, 10)
}
}
ports = append(ports, PortMapping{
Port: cPort,
Binding: PortBinding{HostIP: ip, HostPort: hPort},
})
}
return ports, nil
}

View File

@@ -1,33 +0,0 @@
package nat
import (
"errors"
"strconv"
"strings"
)
// ParsePortRange parses and validates the specified string as a port-range (8000-9000)
func ParsePortRange(ports string) (uint64, uint64, error) {
if ports == "" {
return 0, 0, errors.New("empty string specified for ports")
}
if !strings.Contains(ports, "-") {
start, err := strconv.ParseUint(ports, 10, 16)
end := start
return start, end, err
}
parts := strings.Split(ports, "-")
start, err := strconv.ParseUint(parts[0], 10, 16)
if err != nil {
return 0, 0, err
}
end, err := strconv.ParseUint(parts[1], 10, 16)
if err != nil {
return 0, 0, err
}
if end < start {
return 0, 0, errors.New("invalid range specified for port: " + ports)
}
return start, end, nil
}

View File

@@ -1,96 +0,0 @@
package nat
import (
"sort"
"strings"
)
type portSorter struct {
ports []Port
by func(i, j Port) bool
}
func (s *portSorter) Len() int {
return len(s.ports)
}
func (s *portSorter) Swap(i, j int) {
s.ports[i], s.ports[j] = s.ports[j], s.ports[i]
}
func (s *portSorter) Less(i, j int) bool {
ip := s.ports[i]
jp := s.ports[j]
return s.by(ip, jp)
}
// Sort sorts a list of ports using the provided predicate
// This function should compare `i` and `j`, returning true if `i` is
// considered to be less than `j`
func Sort(ports []Port, predicate func(i, j Port) bool) {
s := &portSorter{ports, predicate}
sort.Sort(s)
}
type portMapEntry struct {
port Port
binding PortBinding
}
type portMapSorter []portMapEntry
func (s portMapSorter) Len() int { return len(s) }
func (s portMapSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Less sorts the port so that the order is:
// 1. port with larger specified bindings
// 2. larger port
// 3. port with tcp protocol
func (s portMapSorter) Less(i, j int) bool {
pi, pj := s[i].port, s[j].port
hpi, hpj := toInt(s[i].binding.HostPort), toInt(s[j].binding.HostPort)
return hpi > hpj || pi.Int() > pj.Int() || (pi.Int() == pj.Int() && strings.ToLower(pi.Proto()) == "tcp")
}
// SortPortMap sorts the list of ports and their respected mapping. The ports
// will explicit HostPort will be placed first.
func SortPortMap(ports []Port, bindings PortMap) {
s := portMapSorter{}
for _, p := range ports {
if binding, ok := bindings[p]; ok && len(binding) > 0 {
for _, b := range binding {
s = append(s, portMapEntry{port: p, binding: b})
}
bindings[p] = []PortBinding{}
} else {
s = append(s, portMapEntry{port: p})
}
}
sort.Sort(s)
var (
i int
pm = make(map[Port]struct{})
)
// reorder ports
for _, entry := range s {
if _, ok := pm[entry.port]; !ok {
ports[i] = entry.port
pm[entry.port] = struct{}{}
i++
}
// reorder bindings for this port
if _, ok := bindings[entry.port]; ok {
bindings[entry.port] = append(bindings[entry.port], entry.binding)
}
}
}
func toInt(s string) uint64 {
i, _, err := ParsePortRange(s)
if err != nil {
i = 0
}
return i
}

View File

@@ -1,24 +0,0 @@
package container
import "github.com/docker/go-connections/nat"
// PortRangeProto is a string containing port number and protocol in the format "80/tcp",
// or a port range and protocol in the format "80-83/tcp".
//
// It is currently an alias for [nat.Port] but may become a concrete type in a future release.
type PortRangeProto = nat.Port
// PortSet is a collection of structs indexed by [HostPort].
//
// It is currently an alias for [nat.PortSet] but may become a concrete type in a future release.
type PortSet = nat.PortSet
// PortBinding represents a binding between a Host IP address and a [HostPort].
//
// It is currently an alias for [nat.PortBinding] but may become a concrete type in a future release.
type PortBinding = nat.PortBinding
// PortMap is a collection of [PortBinding] indexed by [HostPort].
//
// It is currently an alias for [nat.PortMap] but may become a concrete type in a future release.
type PortMap = nat.PortMap

View File

@@ -0,0 +1,345 @@
package container
import (
"errors"
"fmt"
"iter"
"strconv"
"strings"
"unique"
)
// NetworkProtocol represents a network protocol for a port.
type NetworkProtocol string
const (
TCP NetworkProtocol = "tcp"
UDP NetworkProtocol = "udp"
SCTP NetworkProtocol = "sctp"
)
// Sentinel port proto value for zero Port and PortRange values.
var protoZero unique.Handle[NetworkProtocol]
// Port is a type representing a single port number and protocol in the format "<portnum>/[<proto>]".
//
// The zero port value, i.e. Port{}, is invalid; use [ParsePort] to create a valid Port value.
type Port struct {
num uint16
proto unique.Handle[NetworkProtocol]
}
// ParsePort parses s as a [Port].
//
// It normalizes the provided protocol such that "80/tcp", "80/TCP", and "80/tCp" are equivalent.
// If a port number is provided, but no protocol, the default ("tcp") protocol is returned.
func ParsePort(s string) (Port, error) {
if s == "" {
return Port{}, errors.New("invalid port: value is empty")
}
port, proto, _ := strings.Cut(s, "/")
portNum, err := parsePortNumber(port)
if err != nil {
return Port{}, fmt.Errorf("invalid port '%s': %w", port, err)
}
normalizedPortProto := normalizePortProto(proto)
return Port{num: portNum, proto: normalizedPortProto}, nil
}
// MustParsePort calls [ParsePort](s) and panics on error.
//
// It is intended for use in tests with hard-coded strings.
func MustParsePort(s string) Port {
p, err := ParsePort(s)
if err != nil {
panic(err)
}
return p
}
// PortFrom returns a [Port] with the given number and protocol.
//
// If no protocol is specified (i.e. proto == ""), then PortFrom returns Port{}, false.
func PortFrom(num uint16, proto NetworkProtocol) (p Port, ok bool) {
if proto == "" {
return Port{}, false
}
normalized := normalizePortProto(string(proto))
return Port{num: num, proto: normalized}, true
}
// Num returns p's port number.
func (p Port) Num() uint16 {
return p.num
}
// Proto returns p's network protocol.
func (p Port) Proto() NetworkProtocol {
return p.proto.Value()
}
// IsZero reports whether p is the zero value.
func (p Port) IsZero() bool {
return p.proto == protoZero
}
// IsValid reports whether p is an initialized valid port (not the zero value).
func (p Port) IsValid() bool {
return p.proto != protoZero
}
// String returns a string representation of the port in the format "<portnum>/<proto>".
// If the port is the zero value, it returns "invalid port".
func (p Port) String() string {
switch p.proto {
case protoZero:
return "invalid port"
default:
return string(p.AppendTo(nil))
}
}
// AppendText implements [encoding.TextAppender] interface.
// It is the same as [Port.AppendTo] but returns an error to satisfy the interface.
func (p Port) AppendText(b []byte) ([]byte, error) {
return p.AppendTo(b), nil
}
// AppendTo appends a text encoding of p to b and returns the extended buffer.
func (p Port) AppendTo(b []byte) []byte {
if p.IsZero() {
return b
}
return fmt.Appendf(b, "%d/%s", p.num, p.proto.Value())
}
// MarshalText implements [encoding.TextMarshaler] interface.
func (p Port) MarshalText() ([]byte, error) {
return p.AppendText(nil)
}
// UnmarshalText implements [encoding.TextUnmarshaler] interface.
func (p *Port) UnmarshalText(text []byte) error {
if len(text) == 0 {
*p = Port{}
return nil
}
port, err := ParsePort(string(text))
if err != nil {
return err
}
*p = port
return nil
}
// Range returns a [PortRange] representing the single port.
func (p Port) Range() PortRange {
return PortRange{start: p.num, end: p.num, proto: p.proto}
}
// PortSet is a collection of structs indexed by [Port].
type PortSet = map[Port]struct{}
// PortBinding represents a binding between a Host IP address and a Host Port.
type PortBinding struct {
// HostIP is the host IP Address
HostIP string `json:"HostIp"`
// HostPort is the host port number
HostPort string `json:"HostPort"`
}
// PortMap is a collection of [PortBinding] indexed by [Port].
type PortMap = map[Port][]PortBinding
// PortRange represents a range of port numbers and a protocol in the format "8000-9000/tcp".
//
// The zero port range value, i.e. PortRange{}, is invalid; use [ParsePortRange] to create a valid PortRange value.
type PortRange struct {
start uint16
end uint16
proto unique.Handle[NetworkProtocol]
}
// ParsePortRange parses s as a [PortRange].
//
// It normalizes the provided protocol such that "80-90/tcp", "80-90/TCP", and "80-90/tCp" are equivalent.
// If a port number range is provided, but no protocol, the default ("tcp") protocol is returned.
func ParsePortRange(s string) (PortRange, error) {
if s == "" {
return PortRange{}, errors.New("invalid port range: value is empty")
}
portRange, proto, _ := strings.Cut(s, "/")
start, end, ok := strings.Cut(portRange, "-")
startVal, err := parsePortNumber(start)
if err != nil {
return PortRange{}, fmt.Errorf("invalid start port '%s': %w", start, err)
}
portProto := normalizePortProto(proto)
if !ok || start == end {
return PortRange{start: startVal, end: startVal, proto: portProto}, nil
}
endVal, err := parsePortNumber(end)
if err != nil {
return PortRange{}, fmt.Errorf("invalid end port '%s': %w", end, err)
}
if endVal < startVal {
return PortRange{}, errors.New("invalid port range: " + s)
}
return PortRange{start: startVal, end: endVal, proto: portProto}, nil
}
// MustParsePortRange calls [ParsePortRange](s) and panics on error.
// It is intended for use in tests with hard-coded strings.
func MustParsePortRange(s string) PortRange {
pr, err := ParsePortRange(s)
if err != nil {
panic(err)
}
return pr
}
// PortRangeFrom returns a [PortRange] with the given start and end port numbers and protocol.
//
// If end < start or no protocol is specified (i.e. proto == ""), then PortRangeFrom returns PortRange{}, false.
func PortRangeFrom(start, end uint16, proto NetworkProtocol) (pr PortRange, ok bool) {
if end < start || proto == "" {
return PortRange{}, false
}
normalized := normalizePortProto(string(proto))
return PortRange{start: start, end: end, proto: normalized}, true
}
// Start returns pr's start port number.
func (pr PortRange) Start() uint16 {
return pr.start
}
// End returns pr's end port number.
func (pr PortRange) End() uint16 {
return pr.end
}
// Proto returns pr's network protocol.
func (pr PortRange) Proto() NetworkProtocol {
return pr.proto.Value()
}
// IsZero reports whether pr is the zero value.
func (pr PortRange) IsZero() bool {
return pr.proto == protoZero
}
// IsValid reports whether pr is an initialized valid port range (not the zero value).
func (pr PortRange) IsValid() bool {
return pr.proto != protoZero
}
// String returns a string representation of the port range in the format "<start>-<end>/<proto>" or "<portnum>/<proto>" if start == end.
// If the port range is the zero value, it returns "invalid port range".
func (pr PortRange) String() string {
switch pr.proto {
case protoZero:
return "invalid port range"
default:
return string(pr.AppendTo(nil))
}
}
// AppendText implements [encoding.TextAppender] interface.
// It is the same as [PortRange.AppendTo] but returns an error to satisfy the interface.
func (pr PortRange) AppendText(b []byte) ([]byte, error) {
return pr.AppendTo(b), nil
}
// AppendTo appends a text encoding of pr to b and returns the extended buffer.
func (pr PortRange) AppendTo(b []byte) []byte {
if pr.IsZero() {
return b
}
if pr.start == pr.end {
return fmt.Appendf(b, "%d/%s", pr.start, pr.proto.Value())
}
return fmt.Appendf(b, "%d-%d/%s", pr.start, pr.end, pr.proto.Value())
}
// MarshalText implements [encoding.TextMarshaler] interface.
func (pr PortRange) MarshalText() ([]byte, error) {
return pr.AppendText(nil)
}
// UnmarshalText implements [encoding.TextUnmarshaler] interface.
func (pr *PortRange) UnmarshalText(text []byte) error {
if len(text) == 0 {
*pr = PortRange{}
return nil
}
portRange, err := ParsePortRange(string(text))
if err != nil {
return err
}
*pr = portRange
return nil
}
// Range returns pr.
func (pr PortRange) Range() PortRange {
return pr
}
// All returns an iterator over all the individual ports in the range.
//
// For example:
//
// for port := range pr.All() {
// // ...
// }
func (pr PortRange) All() iter.Seq[Port] {
return func(yield func(Port) bool) {
for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ {
if !yield(Port{num: uint16(i), proto: pr.proto}) {
return
}
}
}
}
// parsePortNumber parses rawPort into an int, unwrapping strconv errors
// and returning a single "out of range" error for any value outside 065535.
func parsePortNumber(rawPort string) (uint16, error) {
if rawPort == "" {
return 0, errors.New("value is empty")
}
port, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return 0, err
}
return uint16(port), nil
}
// normalizePortProto normalizes the protocol string such that "tcp", "TCP", and "tCp" are equivalent.
// If proto is not specified, it defaults to "tcp".
func normalizePortProto(proto string) unique.Handle[NetworkProtocol] {
if proto == "" {
return unique.Make(TCP)
}
proto = strings.ToLower(proto)
return unique.Make(NetworkProtocol(proto))
}

1
vendor/modules.txt vendored
View File

@@ -506,7 +506,6 @@ github.com/docker/distribution/registry/storage/cache
github.com/docker/distribution/registry/storage/cache/memory
# github.com/docker/go-connections v0.6.0
## explicit; go 1.18
github.com/docker/go-connections/nat
github.com/docker/go-connections/sockets
github.com/docker/go-connections/tlsconfig
# github.com/docker/go-events v0.0.0-20250808211157-605354379745