diff --git a/cli/command/builder/client_test.go b/cli/command/builder/client_test.go index 2568fca500af..f48914bdb76c 100644 --- a/cli/command/builder/client_test.go +++ b/cli/command/builder/client_test.go @@ -3,18 +3,17 @@ package builder import ( "context" - "github.com/moby/moby/api/types/build" "github.com/moby/moby/client" ) type fakeClient struct { client.Client - builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (*build.CachePruneReport, error) + builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) } -func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (*build.CachePruneReport, error) { +func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) { if c.builderPruneFunc != nil { return c.builderPruneFunc(ctx, opts) } - return nil, nil + return client.BuildCachePruneResult{}, nil } diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index 1619ef928364..962112e92e74 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -87,7 +87,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } } - report, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{ + resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{ All: options.all, ReservedSpace: options.reservedSpace.Value(), Filters: pruneFilters, @@ -95,7 +95,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) if err != nil { return 0, "", err } - + report := resp.Report if len(report.CachesDeleted) > 0 { var sb strings.Builder sb.WriteString("Deleted build cache objects:\n") diff --git a/cli/command/builder/prune_test.go b/cli/command/builder/prune_test.go index c586c237c745..884523197e2b 100644 --- a/cli/command/builder/prune_test.go +++ b/cli/command/builder/prune_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/moby/moby/api/types/build" "github.com/moby/moby/client" ) @@ -16,8 +15,8 @@ func TestBuilderPromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (*build.CachePruneReport, error) { - return nil, errors.New("fakeClient builderPruneFunc should not be called") + builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) { + return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called") }, }) cmd := newPruneCommand(cli) diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go index a3aa58d2f30b..a8d1c2f9083f 100644 --- a/cli/command/container/opts.go +++ b/cli/command/container/opts.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/netip" "os" "path" "path/filepath" @@ -425,15 +426,46 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con entrypoint = []string{""} } + // TODO(thaJeztah): remove uses of go-connections/nat here. convertedOpts, err := convertToStandardNotation(copts.publish.GetSlice()) if err != nil { return nil, err } - ports, portBindings, err := nat.ParsePortSpecs(convertedOpts) + ports, natPortBindings, err := nat.ParsePortSpecs(convertedOpts) if err != nil { return nil, err } + portBindings := network.PortMap{} + for port, bindings := range natPortBindings { + p, err := network.ParsePort(string(port)) + if err != nil { + return nil, err + } + portBindings[p] = []network.PortBinding{} + for _, b := range bindings { + var hostIP netip.Addr + if b.HostIP != "" { + hostIP, err = netip.ParseAddr(b.HostIP) + if err != nil { + return nil, err + } + } + portBindings[p] = append(portBindings[p], network.PortBinding{ + HostIP: hostIP, + HostPort: b.HostPort, + }) + } + } + + exposedPorts := network.PortSet{} + for port := range ports { + p, err := network.ParsePort(string(port)) + if err != nil { + return nil, err + } + exposedPorts[p] = struct{}{} + } // Merge in exposed ports to the map of published ports for _, e := range copts.expose.GetSlice() { @@ -625,7 +657,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con config := &container.Config{ Hostname: copts.hostname, Domainname: copts.domainname, - ExposedPorts: ports, + ExposedPorts: exposedPorts, User: copts.user, Tty: copts.tty, OpenStdin: copts.stdin, @@ -661,7 +693,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con // but pre created containers can still have those nil values. // See https://github.com/docker/docker/pull/17779 // for a more detailed explanation on why we don't want that. - DNS: copts.dns.GetAllOrEmpty(), + DNS: toNetipAddrSlice(copts.dns.GetAllOrEmpty()), DNSSearch: copts.dnsSearch.GetAllOrEmpty(), DNSOptions: copts.dnsOptions.GetAllOrEmpty(), ExtraHosts: copts.extraHosts.GetSlice(), @@ -804,10 +836,10 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption if len(n.Links) > 0 && copts.links.Len() > 0 { return invalidParameter(errors.New("conflicting options: cannot specify both --link and per-network links")) } - if n.IPv4Address != "" && copts.ipv4Address != "" { + if n.IPv4Address.IsValid() && copts.ipv4Address != "" { return invalidParameter(errors.New("conflicting options: cannot specify both --ip and per-network IPv4 address")) } - if n.IPv6Address != "" && copts.ipv6Address != "" { + if n.IPv6Address.IsValid() && copts.ipv6Address != "" { return invalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address")) } if n.MacAddress != "" && copts.macAddress != "" { @@ -827,17 +859,24 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption copy(n.Links, copts.links.GetSlice()) } if copts.ipv4Address != "" { - n.IPv4Address = copts.ipv4Address + var err error + n.IPv4Address, err = netip.ParseAddr(copts.ipv4Address) + if err != nil { + return err + } } if copts.ipv6Address != "" { - n.IPv6Address = copts.ipv6Address + var err error + n.IPv6Address, err = netip.ParseAddr(copts.ipv6Address) + if err != nil { + return err + } } if copts.macAddress != "" { n.MacAddress = copts.macAddress } if copts.linkLocalIPs.Len() > 0 { - n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) - copy(n.LinkLocalIPs, copts.linkLocalIPs.GetSlice()) + n.LinkLocalIPs = toNetipAddrSlice(copts.linkLocalIPs.GetSlice()) } return nil } @@ -866,7 +905,7 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.Endpoint if len(ep.Links) > 0 { epConfig.Links = ep.Links } - if ep.IPv4Address != "" || ep.IPv6Address != "" || len(ep.LinkLocalIPs) > 0 { + if ep.IPv4Address.IsValid() || ep.IPv6Address.IsValid() || len(ep.LinkLocalIPs) > 0 { epConfig.IPAMConfig = &network.EndpointIPAMConfig{ IPv4Address: ep.IPv4Address, IPv6Address: ep.IPv6Address, @@ -1130,3 +1169,15 @@ func validateAttach(val string) (string, error) { } return val, errors.New("valid streams are STDIN, STDOUT and STDERR") } + +func toNetipAddrSlice(ips []string) []netip.Addr { + netips := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + continue + } + netips = append(netips, addr) + } + return netips +} diff --git a/cli/command/container/opts_test.go b/cli/command/container/opts_test.go index 44dce2d26443..083fd57fa1f9 100644 --- a/cli/command/container/opts_test.go +++ b/cli/command/container/opts_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "net/netip" "os" "runtime" "strings" @@ -438,12 +439,12 @@ func TestParseWithExpose(t *testing.T) { "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, } - valids := map[string][]container.PortRangeProto{ - "8080/tcp": {"8080/tcp"}, - "8080/udp": {"8080/udp"}, - "8080/ncp": {"8080/ncp"}, - "8080-8080/udp": {"8080/udp"}, - "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, + valids := map[string][]networktypes.Port{ + "8080/tcp": {networktypes.MustParsePort("8080/tcp")}, + "8080/udp": {networktypes.MustParsePort("8080/udp")}, + "8080/ncp": {networktypes.MustParsePort("8080/ncp")}, + "8080-8080/udp": {networktypes.MustParsePort("8080/udp")}, + "8080-8082/tcp": {networktypes.MustParsePort("8080/tcp"), networktypes.MustParsePort("8081/tcp"), networktypes.MustParsePort("8082/tcp")}, } for expose, expectedError := range invalids { if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { @@ -472,7 +473,7 @@ func TestParseWithExpose(t *testing.T) { if len(config.ExposedPorts) != 2 { t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts) } - ports := []container.PortRangeProto{"80/tcp", "81/tcp"} + ports := []networktypes.Port{networktypes.MustParsePort("80/tcp"), networktypes.MustParsePort("81/tcp")} for _, port := range ports { if _, ok := config.ExposedPorts[port]; !ok { t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) @@ -607,9 +608,9 @@ func TestParseNetworkConfig(t *testing.T) { expected: map[string]*networktypes.EndpointSettings{ "net1": { IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", - LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")}, }, Links: []string{"foo:bar", "bar:baz"}, Aliases: []string{"web1", "web2"}, @@ -637,9 +638,9 @@ func TestParseNetworkConfig(t *testing.T) { "net1": { DriverOpts: map[string]string{"field1": "value1"}, IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", - LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")}, }, Links: []string{"foo:bar", "bar:baz"}, Aliases: []string{"web1", "web2"}, @@ -648,15 +649,15 @@ func TestParseNetworkConfig(t *testing.T) { "net3": { DriverOpts: map[string]string{"field3": "value3"}, IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), }, Aliases: []string{"web3"}, }, "net4": { MacAddress: "02:32:1c:23:00:04", IPAMConfig: &networktypes.EndpointIPAMConfig{ - LinkLocalIPs: []string{"169.254.169.254"}, + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254")}, }, }, }, @@ -672,8 +673,8 @@ func TestParseNetworkConfig(t *testing.T) { "field2": "value2", }, IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), }, Aliases: []string{"web1", "web2"}, MacAddress: "02:32:1c:23:00:04", diff --git a/cli/command/container/port.go b/cli/command/container/port.go index 1df09417df9a..a10268b3bdf6 100644 --- a/cli/command/container/port.go +++ b/cli/command/container/port.go @@ -5,14 +5,13 @@ import ( "fmt" "net" "sort" - "strconv" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/fvbommel/sortorder" - "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/spf13/cobra" ) @@ -60,24 +59,21 @@ func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) erro var out []string if opts.port != "" { - port, proto, _ := strings.Cut(opts.port, "/") - if proto == "" { - proto = "tcp" + port, err := network.ParsePort(opts.port) + if err != nil { + return err } - if _, err = strconv.ParseUint(port, 10, 16); err != nil { - return fmt.Errorf("invalid port (%s): %w", port, err) - } - frontends, exists := c.NetworkSettings.Ports[container.PortRangeProto(port+"/"+proto)] + frontends, exists := c.NetworkSettings.Ports[port] if !exists || len(frontends) == 0 { return fmt.Errorf("no public port '%s' published for %s", opts.port, opts.container) } for _, frontend := range frontends { - out = append(out, net.JoinHostPort(frontend.HostIP, frontend.HostPort)) + out = append(out, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort)) } } else { for from, frontends := range c.NetworkSettings.Ports { for _, frontend := range frontends { - out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort))) + out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort))) } } } diff --git a/cli/command/container/port_test.go b/cli/command/container/port_test.go index e1a7e317ca2f..aaed4a03540c 100644 --- a/cli/command/container/port_test.go +++ b/cli/command/container/port_test.go @@ -2,10 +2,12 @@ package container import ( "io" + "net/netip" "testing" "github.com/docker/cli/internal/test" "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -13,32 +15,32 @@ import ( func TestNewPortCommandOutput(t *testing.T) { testCases := []struct { name string - ips []string + ips []netip.Addr port string }{ { name: "container-port-ipv4", - ips: []string{"0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("0.0.0.0")}, port: "80", }, { name: "container-port-ipv6", - ips: []string{"::"}, + ips: []netip.Addr{netip.MustParseAddr("::")}, port: "80", }, { name: "container-port-ipv6-and-ipv4", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, port: "80", }, { name: "container-port-ipv6-and-ipv4-443-udp", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, port: "443/udp", }, { name: "container-port-all-ports", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, }, } for _, tc := range testCases { @@ -46,19 +48,19 @@ func TestNewPortCommandOutput(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ inspectFunc: func(string) (container.InspectResponse, error) { ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}} - ci.NetworkSettings.Ports = container.PortMap{ - "80/tcp": make([]container.PortBinding, len(tc.ips)), - "443/tcp": make([]container.PortBinding, len(tc.ips)), - "443/udp": make([]container.PortBinding, len(tc.ips)), + ci.NetworkSettings.Ports = network.PortMap{ + network.MustParsePort("80/tcp"): make([]network.PortBinding, len(tc.ips)), + network.MustParsePort("443/tcp"): make([]network.PortBinding, len(tc.ips)), + network.MustParsePort("443/udp"): make([]network.PortBinding, len(tc.ips)), } for i, ip := range tc.ips { - ci.NetworkSettings.Ports["80/tcp"][i] = container.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("80/tcp")][i] = network.PortBinding{ HostIP: ip, HostPort: "3456", } - ci.NetworkSettings.Ports["443/tcp"][i] = container.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("443/tcp")][i] = network.PortBinding{ HostIP: ip, HostPort: "4567", } - ci.NetworkSettings.Ports["443/udp"][i] = container.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("443/udp")][i] = network.PortBinding{ HostIP: ip, HostPort: "5678", } } diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 60dfb91b42f2..9ce7fbf68f06 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -360,13 +360,13 @@ func DisplayablePorts(ports []container.PortSummary) string { for _, port := range ports { current := port.PrivatePort portKey := port.Type - if port.IP != "" { + if !port.IP.IsUnspecified() { if port.PublicPort != current { - hAddrPort := net.JoinHostPort(port.IP, strconv.Itoa(int(port.PublicPort))) + hAddrPort := net.JoinHostPort(port.IP.String(), strconv.Itoa(int(port.PublicPort))) hostMappings = append(hostMappings, fmt.Sprintf("%s->%d/%s", hAddrPort, port.PrivatePort, port.Type)) continue } - portKey = port.IP + "/" + port.Type + portKey = port.IP.String() + "/" + port.Type } group := groupMap[portKey] @@ -416,7 +416,7 @@ func comparePorts(i, j container.PortSummary) bool { } if i.IP != j.IP { - return i.IP < j.IP + return i.IP.String() < j.IP.String() } if i.PublicPort != j.PublicPort { diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 594ced3b47d0..67c76cff97aa 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "fmt" + "net/netip" "strings" "testing" "time" @@ -660,7 +661,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "0.0.0.0", + IP: netip.MustParseAddr("0.0.0.0"), PrivatePort: 9988, Type: "tcp", }, @@ -670,7 +671,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::", + IP: netip.MustParseAddr("::"), PrivatePort: 9988, Type: "tcp", }, @@ -690,7 +691,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", @@ -701,7 +702,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", @@ -712,7 +713,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 9988, PublicPort: 9988, Type: "tcp", @@ -723,7 +724,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 9988, PublicPort: 9988, Type: "tcp", @@ -746,12 +747,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 9998, PrivatePort: 9998, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 9999, PrivatePort: 9999, Type: "udp", @@ -762,12 +763,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 9998, PrivatePort: 9998, Type: "udp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 9999, PrivatePort: 9999, Type: "udp", @@ -778,12 +779,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 8887, PrivatePort: 9998, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 8888, PrivatePort: 9999, Type: "udp", @@ -794,12 +795,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 8887, PrivatePort: 9998, Type: "udp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 8888, PrivatePort: 9999, Type: "udp", @@ -822,7 +823,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 6677, PublicPort: 7766, Type: "tcp", @@ -837,22 +838,22 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 9988, PublicPort: 8899, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", }, { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", @@ -867,12 +868,12 @@ func TestDisplayablePorts(t *testing.T) { PublicPort: 8899, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 6677, PublicPort: 7766, Type: "tcp", }, { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", @@ -895,42 +896,42 @@ func TestDisplayablePorts(t *testing.T) { PrivatePort: 1024, Type: "udp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "tcp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "udp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "tcp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "udp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "tcp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "udp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "tcp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "udp", diff --git a/cli/command/network/connect.go b/cli/command/network/connect.go index dc9d96ae50f6..211087b5d16e 100644 --- a/cli/command/network/connect.go +++ b/cli/command/network/connect.go @@ -3,6 +3,8 @@ package network import ( "context" "errors" + "net" + "net/netip" "strings" "github.com/docker/cli/cli" @@ -17,11 +19,11 @@ import ( type connectOptions struct { network string container string - ipaddress string - ipv6address string + ipaddress net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly + ipv6address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly links opts.ListOpts aliases []string - linklocalips []string + linklocalips []net.IP // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly driverOpts []string gwPriority int } @@ -51,11 +53,11 @@ func newConnectCommand(dockerCLI command.Cli) *cobra.Command { } flags := cmd.Flags() - flags.StringVar(&options.ipaddress, "ip", "", `IPv4 address (e.g., "172.30.100.104")`) - flags.StringVar(&options.ipv6address, "ip6", "", `IPv6 address (e.g., "2001:db8::33")`) + flags.IPVar(&options.ipaddress, "ip", nil, `IPv4 address (e.g., "172.30.100.104")`) + flags.IPVar(&options.ipv6address, "ip6", nil, `IPv6 address (e.g., "2001:db8::33")`) flags.Var(&options.links, "link", "Add link to another container") flags.StringSliceVar(&options.aliases, "alias", []string{}, "Add network-scoped alias for the container") - flags.StringSliceVar(&options.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container") + flags.IPSliceVar(&options.linklocalips, "link-local-ip", nil, "Add a link-local address for the container") flags.StringSliceVar(&options.driverOpts, "driver-opt", []string{}, "driver options for the network") flags.IntVar(&options.gwPriority, "gw-priority", 0, "Highest gw-priority provides the default gateway. Accepts positive and negative values.") return cmd @@ -69,9 +71,9 @@ func runConnect(ctx context.Context, apiClient client.NetworkAPIClient, options return apiClient.NetworkConnect(ctx, options.network, options.container, &network.EndpointSettings{ IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: options.ipaddress, - IPv6Address: options.ipv6address, - LinkLocalIPs: options.linklocalips, + IPv4Address: toNetipAddr(options.ipaddress), + IPv6Address: toNetipAddr(options.ipv6address), + LinkLocalIPs: toNetipAddrSlice(options.linklocalips), }, Links: options.links.GetSlice(), Aliases: options.aliases, @@ -93,3 +95,48 @@ func convertDriverOpt(options []string) (map[string]string, error) { } return driverOpt, nil } + +func toNetipAddrSlice(ips []net.IP) []netip.Addr { + netips := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + netips = append(netips, toNetipAddr(ip)) + } + return netips +} + +func toNetipAddr(ip net.IP) netip.Addr { + if len(ip) == 0 { + return netip.Addr{} + } + if ip4 := ip.To4(); ip4 != nil { + a, _ := netip.AddrFromSlice(ip4) + return a + } + if ip16 := ip.To16(); ip16 != nil { + a, _ := netip.AddrFromSlice(ip16) + return a + } + return netip.Addr{} +} + +func ipNetToPrefix(n net.IPNet) netip.Prefix { + if n.IP == nil { + return netip.Prefix{} + } + + ip := n.IP.To4() + if ip == nil { + ip = n.IP.To16() + } + if ip == nil { + return netip.Prefix{} + } + + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return netip.Prefix{} + } + + ones, _ := n.Mask.Size() + return netip.PrefixFrom(addr, ones) +} diff --git a/cli/command/network/connect_test.go b/cli/command/network/connect_test.go index f1176a031ed4..81777c5a7fce 100644 --- a/cli/command/network/connect_test.go +++ b/cli/command/network/connect_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "net/netip" "testing" "github.com/docker/cli/internal/test" @@ -46,9 +47,9 @@ func TestNetworkConnectErrors(t *testing.T) { func TestNetworkConnectWithFlags(t *testing.T) { expectedConfig := &network.EndpointSettings{ IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: "192.168.4.1", - IPv6Address: "fdef:f401:8da0:1234::5678", - LinkLocalIPs: []string{"169.254.42.42"}, + IPv4Address: netip.MustParseAddr("192.168.4.1"), + IPv6Address: netip.MustParseAddr("fdef:f401:8da0:1234::5678"), + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.42.42")}, }, Links: []string{"otherctr"}, Aliases: []string{"poor-yorick"}, diff --git a/cli/command/network/create.go b/cli/command/network/create.go index e0798584d42a..482c16859785 100644 --- a/cli/command/network/create.go +++ b/cli/command/network/create.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net" + "net/netip" "strings" "github.com/docker/cli/cli" @@ -34,9 +35,9 @@ type createOptions struct { type ipamOptions struct { driver string - subnets []string - ipRanges []string - gateways []string + subnets []string // TODO(thaJeztah): change to []net.IPNet? This won't accept a bare address (without "/xxx"); we need a flag-type to handle []netip.Prefix directly + ipRanges []net.IPNet // TODO(thaJeztah): we need a flag-type to handle []netip.Prefix directly + gateways []net.IP // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly auxAddresses opts.MapOpts driverOpts opts.MapOpts } @@ -92,8 +93,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { flags.StringVar(&options.ipam.driver, "ipam-driver", "default", "IP Address Management Driver") flags.StringSliceVar(&options.ipam.subnets, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") - flags.StringSliceVar(&options.ipam.ipRanges, "ip-range", []string{}, "Allocate container ip from a sub-range") - flags.StringSliceVar(&options.ipam.gateways, "gateway", []string{}, "IPv4 or IPv6 Gateway for the master subnet") + flags.IPNetSliceVar(&options.ipam.ipRanges, "ip-range", nil, "Allocate container ip from a sub-range") + flags.IPSliceVar(&options.ipam.gateways, "gateway", nil, "IPv4 or IPv6 Gateway for the master subnet") flags.Var(&options.ipam.auxAddresses, "aux-address", "Auxiliary IPv4 or IPv6 addresses used by Network driver") flags.Var(&options.ipam.driverOpts, "ipam-opt", "Set IPAM driver specific options") @@ -149,6 +150,7 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) { // Populate non-overlapping subnets into consolidation map for _, s := range options.subnets { + // TODO(thaJeztah): is all this validation needed on the CLI-side? for k := range iData { ok1, err := subnetMatches(s, k) if err != nil { @@ -162,28 +164,32 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) { return nil, errors.New("multiple overlapping subnet configuration is not supported") } } - iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}} + sn, err := parsePrefixOrAddr(s) + if err != nil { + return nil, err + } + iData[s] = &network.IPAMConfig{Subnet: sn, AuxAddress: map[string]netip.Addr{}} } // Validate and add valid ip ranges for _, r := range options.ipRanges { + // TODO(thaJeztah): is all this validation needed on the CLI-side? match := false for _, s := range options.subnets { - if _, _, err := net.ParseCIDR(r); err != nil { - return nil, err - } - ok, err := subnetMatches(s, r) + ok, err := subnetMatches(s, r.String()) if err != nil { return nil, err } if !ok { continue } - if iData[s].IPRange != "" { + + // Using "IsValid" to check if a valid IPRange was already set. + if iData[s].IPRange.IsValid() { return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) } d := iData[s] - d.IPRange = r + d.IPRange = ipNetToPrefix(r) match = true } if !match { @@ -195,18 +201,19 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) { for _, g := range options.gateways { match := false for _, s := range options.subnets { - ok, err := subnetMatches(s, g) + // TODO(thaJeztah): is all this validation needed on the CLI-side? + ok, err := subnetMatches(s, g.String()) if err != nil { return nil, err } if !ok { continue } - if iData[s].Gateway != "" { + if iData[s].Gateway.IsValid() { return nil, fmt.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) } d := iData[s] - d.Gateway = g + d.Gateway = toNetipAddr(g) match = true } if !match { @@ -216,16 +223,20 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) { // Validate and add aux-addresses for key, aa := range options.auxAddresses.GetAll() { + auxAddr, err := netip.ParseAddr(aa) + if err != nil { + return nil, err + } match := false for _, s := range options.subnets { - ok, err := subnetMatches(s, aa) + ok, err := subnetMatches(s, auxAddr.String()) if err != nil { return nil, err } if !ok { continue } - iData[s].AuxAddress[key] = aa + iData[s].AuxAddress[key] = auxAddr match = true } if !match { @@ -264,3 +275,20 @@ func subnetMatches(subnet, data string) (bool, error) { return s.Contains(ip), nil } + +// parsePrefixOrAddr parses s as a subnet in CIDR notation (e.g. "10.0.0.0/24"). +// If s does not include a prefix length, it is interpreted as a single-address +// subnet using the full address width (/32 for IPv4 or /128 for IPv6). +// +// It returns the resulting netip.Prefix or an error if the input is invalid. +func parsePrefixOrAddr(s string) (netip.Prefix, error) { + pfx, err := netip.ParsePrefix(s) + if err != nil { + addr, err := netip.ParseAddr(s) + if err != nil { + return netip.Prefix{}, fmt.Errorf("invalid address: %w", err) + } + pfx = netip.PrefixFrom(addr, addr.BitLen()) + } + return pfx, nil +} diff --git a/cli/command/network/create_test.go b/cli/command/network/create_test.go index eb4bff468f31..43a02bd80396 100644 --- a/cli/command/network/create_test.go +++ b/cli/command/network/create_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "net/netip" "strings" "testing" @@ -156,10 +157,10 @@ func TestNetworkCreateWithFlags(t *testing.T) { expectedDriver := "foo" expectedOpts := []network.IPAMConfig{ { - Subnet: "192.168.4.0/24", - IPRange: "192.168.4.0/24", - Gateway: "192.168.4.1/24", - AuxAddress: map[string]string{}, + Subnet: netip.MustParsePrefix("192.168.4.0/24"), + IPRange: netip.MustParsePrefix("192.168.4.0/24"), + Gateway: netip.MustParseAddr("192.168.4.1"), // TODO(thaJeztah): this was testing with "192.168.4.1/24", but it now only accepts an address? + AuxAddress: map[string]netip.Addr{}, }, } cli := test.NewFakeCli(&fakeClient{ diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index ba00600a995c..778536a0d69a 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -735,7 +735,7 @@ type portRange struct { pEnd uint32 tStart uint32 tEnd uint32 - protocol swarm.PortConfigProtocol + protocol network.IPProtocol } func (pr portRange) String() string { diff --git a/cli/command/service/inspect_test.go b/cli/command/service/inspect_test.go index fa09a64ce6c6..8fb0a40620d3 100644 --- a/cli/command/service/inspect_test.go +++ b/cli/command/service/inspect_test.go @@ -6,6 +6,7 @@ package service import ( "bytes" "encoding/json" + "net/netip" "strings" "testing" "time" @@ -27,7 +28,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Mode: "vip", Ports: []swarm.PortConfig{ { - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, TargetPort: 5000, }, }, @@ -108,7 +109,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Spec: *endpointSpec, Ports: []swarm.PortConfig{ { - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, TargetPort: 5000, PublishedPort: 30000, }, @@ -116,7 +117,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) VirtualIPs: []swarm.EndpointVirtualIP{ { NetworkID: "6o4107cj2jx9tihgb0jyts6pj", - Addr: "10.255.0.4/16", + Addr: netip.MustParseAddr("10.255.0.4/16"), // FIXME(thaJeztah): this was testing with "10.255.0.4/16" }, }, }, diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 2dc89fffcb92..d276b2c10042 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "net/netip" "sort" "strconv" "strings" @@ -762,7 +763,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N Mounts: options.mounts.Value(), Init: &options.init, DNSConfig: &swarm.DNSConfig{ - Nameservers: options.dns.GetSlice(), + Nameservers: toNetipAddrSlice(options.dns.GetSlice()), Search: options.dnsSearch.GetSlice(), Options: options.dnsOption.GetSlice(), }, @@ -1073,3 +1074,15 @@ const ( flagUlimitRemove = "ulimit-rm" flagOomScoreAdj = "oom-score-adj" ) + +func toNetipAddrSlice(ips []string) []netip.Addr { + netips := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + continue + } + netips = append(netips, addr) + } + return netips +} diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 15e33419fa77..f4b1a268aefb 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "net/netip" + "slices" "sort" "strings" "time" @@ -15,6 +17,7 @@ import ( "github.com/docker/cli/opts/swarmopts" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/api/types/versions" "github.com/moby/moby/client" @@ -999,42 +1002,45 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error { return nil } -func removeDuplicates(entries []string) []string { - hit := map[string]bool{} - newEntries := []string{} - for _, v := range entries { - if !hit[v] { - newEntries = append(newEntries, v) - hit[v] = true - } - } - return newEntries -} - func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { newConfig := &swarm.DNSConfig{} nameservers := (*config).Nameservers if flags.Changed(flagDNSAdd) { values := flags.Lookup(flagDNSAdd).Value.(*opts.ListOpts).GetSlice() - nameservers = append(nameservers, values...) + var ips []netip.Addr + for _, ip := range values { + a, err := netip.ParseAddr(ip) + if err != nil { + return err + } + ips = append(ips, a) + } + nameservers = append(nameservers, ips...) } - nameservers = removeDuplicates(nameservers) + // Remove duplicates + slices.SortFunc(newConfig.Nameservers, func(a, b netip.Addr) int { + return a.Compare(b) + }) + nameservers = slices.Compact(nameservers) + toRemove := buildToRemoveSet(flags, flagDNSRemove) for _, nameserver := range nameservers { - if _, exists := toRemove[nameserver]; !exists { + if _, exists := toRemove[nameserver.String()]; !exists { newConfig.Nameservers = append(newConfig.Nameservers, nameserver) } } // Sort so that result is predictable. - sort.Strings(newConfig.Nameservers) + slices.SortFunc(newConfig.Nameservers, func(a, b netip.Addr) int { + return a.Compare(b) + }) search := (*config).Search if flags.Changed(flagDNSSearchAdd) { values := flags.Lookup(flagDNSSearchAdd).Value.(*opts.ListOpts).GetSlice() search = append(search, values...) } - search = removeDuplicates(search) + search = slices.Compact(search) toRemove = buildToRemoveSet(flags, flagDNSSearchRemove) for _, entry := range search { if _, exists := toRemove[entry]; !exists { @@ -1049,7 +1055,7 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { values := flags.Lookup(flagDNSOptionAdd).Value.(*opts.ListOpts).GetSlice() options = append(options, values...) } - options = removeDuplicates(options) + options = slices.Compact(options) toRemove = buildToRemoveSet(flags, flagDNSOptionRemove) for _, option := range options { if _, exists := toRemove[option]; !exists { @@ -1120,16 +1126,16 @@ portLoop: return nil } -func equalProtocol(prot1, prot2 swarm.PortConfigProtocol) bool { +func equalProtocol(prot1, prot2 network.IPProtocol) bool { return prot1 == prot2 || - (prot1 == swarm.PortConfigProtocol("") && prot2 == swarm.PortConfigProtocolTCP) || - (prot2 == swarm.PortConfigProtocol("") && prot1 == swarm.PortConfigProtocolTCP) + (prot1 == "" && prot2 == network.TCP) || + (prot2 == "" && prot1 == network.TCP) } func equalPublishMode(mode1, mode2 swarm.PortConfigPublishMode) bool { return mode1 == mode2 || - (mode1 == swarm.PortConfigPublishMode("") && mode2 == swarm.PortConfigPublishModeIngress) || - (mode2 == swarm.PortConfigPublishMode("") && mode1 == swarm.PortConfigPublishModeIngress) + (mode1 == "" && mode2 == swarm.PortConfigPublishModeIngress) || + (mode2 == "" && mode1 == swarm.PortConfigPublishModeIngress) } func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error { @@ -1232,7 +1238,9 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error { values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetSlice()) newHosts = append(newHosts, values...) } - *hosts = removeDuplicates(newHosts) + + slices.Sort(newHosts) + *hosts = slices.Compact(newHosts) return nil } diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index 542c83adb5b6..c795b121ccf8 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "net/netip" "reflect" "sort" "testing" @@ -213,7 +214,7 @@ func TestUpdateDNSConfig(t *testing.T) { flags.Set("dns-option-rm", "timeout:3") config := &swarm.DNSConfig{ - Nameservers: []string{"3.3.3.3", "5.5.5.5"}, + Nameservers: []netip.Addr{netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("5.5.5.5")}, Search: []string{"localdomain"}, Options: []string{"timeout:3"}, } @@ -272,7 +273,7 @@ func TestUpdatePorts(t *testing.T) { flags.Set("publish-rm", "333/udp") portConfigs := []swarm.PortConfig{ - {TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP}, + {TargetPort: 333, Protocol: network.UDP}, {TargetPort: 555}, } @@ -295,7 +296,7 @@ func TestUpdatePortsDuplicate(t *testing.T) { { TargetPort: 80, PublishedPort: 80, - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, PublishMode: swarm.PortConfigPublishModeIngress, }, } @@ -488,7 +489,7 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) { { TargetPort: 80, PublishedPort: 8080, - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, PublishMode: swarm.PortConfigPublishModeIngress, }, } diff --git a/cli/command/stack/services_test.go b/cli/command/stack/services_test.go index 51d2b8d38e20..0fe5b5fc8e0a 100644 --- a/cli/command/stack/services_test.go +++ b/cli/command/stack/services_test.go @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/client" "gotest.tools/v3/assert" @@ -164,7 +165,7 @@ func TestStackServicesWithoutFormat(t *testing.T) { PublishMode: swarm.PortConfigPublishModeIngress, PublishedPort: 0, TargetPort: 3232, - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, }), )}, nil }, diff --git a/cli/command/swarm/init.go b/cli/command/swarm/init.go index b19b1beb27c9..924c0f5806ae 100644 --- a/cli/command/swarm/init.go +++ b/cli/command/swarm/init.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" "strings" "github.com/docker/cli/cli" @@ -67,9 +68,22 @@ func newInitCommand(dockerCLI command.Cli) *cobra.Command { func runInit(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts initOptions) error { apiClient := dockerCLI.Client() - defaultAddrPool := make([]string, 0, len(opts.defaultAddrPools)) + // TODO(thaJeztah): should we change opts.defaultAddrPools to be the right type? What formats does it accept? + defaultAddrPool := make([]netip.Prefix, 0, len(opts.defaultAddrPools)) for _, p := range opts.defaultAddrPools { - defaultAddrPool = append(defaultAddrPool, p.String()) + if len(p.IP) == 0 { + continue + } + ip := p.IP.To4() + if ip == nil { + ip = p.IP.To16() + } + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return fmt.Errorf("invalid IP address: %s", p.IP) + } + ones, _ := p.Mask.Size() + defaultAddrPool = append(defaultAddrPool, netip.PrefixFrom(addr, ones)) } req := swarm.InitRequest{ ListenAddr: opts.listenAddr.String(), diff --git a/cli/command/system/info.go b/cli/command/system/info.go index 0fcca3d95ef9..989359eea9bf 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -372,9 +372,8 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { } } - for _, registryConfig := range info.RegistryConfig.InsecureRegistryCIDRs { - mask, _ := registryConfig.Mask.Size() - fprintf(output, " %s/%d\n", registryConfig.IP.String(), mask) + for _, cidr := range info.RegistryConfig.InsecureRegistryCIDRs { + fprintf(output, " %s\n", cidr) } } @@ -429,7 +428,7 @@ func printSwarmInfo(output io.Writer, info system.Info) { var strAddrPool strings.Builder if info.Swarm.Cluster.DefaultAddrPool != nil { for _, p := range info.Swarm.Cluster.DefaultAddrPool { - strAddrPool.WriteString(p + " ") + strAddrPool.WriteString(p.String() + " ") } fprintln(output, " Default Address Pool:", strAddrPool.String()) fprintln(output, " SubnetSize:", info.Swarm.Cluster.SubnetSize) diff --git a/cli/command/system/info_test.go b/cli/command/system/info_test.go index 90222a61699f..09278ee206e8 100644 --- a/cli/command/system/info_test.go +++ b/cli/command/system/info_test.go @@ -3,7 +3,7 @@ package system import ( "encoding/base64" "errors" - "net" + "net/netip" "testing" "time" @@ -69,11 +69,8 @@ var sampleInfoNoSwarm = system.Info{ Architecture: "x86_64", IndexServerAddress: "https://index.docker.io/v1/", RegistryConfig: ®istrytypes.ServiceConfig{ - InsecureRegistryCIDRs: []*registrytypes.NetIPNet{ - { - IP: net.ParseIP("127.0.0.0"), - Mask: net.IPv4Mask(255, 0, 0, 0), - }, + InsecureRegistryCIDRs: []netip.Prefix{ + netip.MustParsePrefix("127.0.0.0/8"), }, IndexConfigs: map[string]*registrytypes.IndexInfo{ "docker.io": { @@ -119,7 +116,7 @@ var sampleInfoNoSwarm = system.Info{ SecurityOptions: []string{"name=apparmor", "name=seccomp,profile=default"}, DefaultAddressPools: []system.NetworkAddressPool{ { - Base: "10.123.0.0/16", + Base: netip.MustParsePrefix("10.123.0.0/16"), Size: 24, }, }, diff --git a/cli/compose/convert/compose.go b/cli/compose/convert/compose.go index d2122f8b26b3..74c9c6a6e381 100644 --- a/cli/compose/convert/compose.go +++ b/cli/compose/convert/compose.go @@ -1,6 +1,8 @@ package convert import ( + "fmt" + "net/netip" "os" "strings" @@ -84,8 +86,9 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str Driver: nw.Ipam.Driver, } for _, ipamConfig := range nw.Ipam.Config { + sn, _ := parsePrefixOrAddr(ipamConfig.Subnet) // TODO(thaJeztah): change Subnet field to netip.Prefix (but this would break "address only" formats. createOpts.IPAM.Config = append(createOpts.IPAM.Config, network.IPAMConfig{ - Subnet: ipamConfig.Subnet, + Subnet: sn, }) } } @@ -199,3 +202,20 @@ func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObj Data: data, }, nil } + +// parsePrefixOrAddr parses s as a subnet in CIDR notation (e.g. "10.0.0.0/24"). +// If s does not include a prefix length, it is interpreted as a single-address +// subnet using the full address width (/32 for IPv4 or /128 for IPv6). +// +// It returns the resulting netip.Prefix or an error if the input is invalid. +func parsePrefixOrAddr(s string) (netip.Prefix, error) { + pfx, err := netip.ParsePrefix(s) + if err != nil { + addr, err := netip.ParseAddr(s) + if err != nil { + return netip.Prefix{}, fmt.Errorf("invalid address: %w", err) + } + pfx = netip.PrefixFrom(addr, addr.BitLen()) + } + return pfx, nil +} diff --git a/cli/compose/convert/compose_test.go b/cli/compose/convert/compose_test.go index f3d697b6725d..3567d69ffd54 100644 --- a/cli/compose/convert/compose_test.go +++ b/cli/compose/convert/compose_test.go @@ -1,6 +1,7 @@ package convert import ( + "net/netip" "testing" composetypes "github.com/docker/cli/cli/compose/types" @@ -89,7 +90,7 @@ func TestNetworks(t *testing.T) { Driver: "driver", Config: []network.IPAMConfig{ { - Subnet: "10.0.0.0", + Subnet: netip.MustParsePrefix("10.0.0.0/32"), }, }, }, diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 9714cb372f47..731b2195cca0 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/netip" "os" "sort" "strings" @@ -13,6 +14,7 @@ import ( composetypes "github.com/docker/cli/cli/compose/types" "github.com/docker/cli/opts" "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/api/types/versions" "github.com/moby/moby/client" @@ -96,7 +98,7 @@ func Service( return swarm.ServiceSpec{}, err } - dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch) + dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch) // TODO(thaJeztah): change service.DNS to a []netip.Addr var privileges swarm.Privileges privileges.CredentialSpec, err = convertCredentialSpec( @@ -578,7 +580,7 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC portConfigs := []swarm.PortConfig{} for _, port := range source { portConfig := swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(port.Protocol), + Protocol: network.IPProtocol(port.Protocol), TargetPort: port.Target, PublishedPort: port.Published, PublishMode: swarm.PortConfigPublishMode(port.Mode), @@ -641,15 +643,27 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) } func convertDNSConfig(dns []string, dnsSearch []string) *swarm.DNSConfig { - if dns != nil || dnsSearch != nil { + if len(dns) > 0 || len(dnsSearch) > 0 { return &swarm.DNSConfig{ - Nameservers: dns, + Nameservers: toNetipAddrSlice(dns), Search: dnsSearch, } } return nil } +func toNetipAddrSlice(ips []string) []netip.Addr { + netips := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + continue + } + netips = append(netips, addr) + } + return netips +} + func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) { var o []string diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index 82db561577e1..e6107d36e5fa 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -307,7 +307,7 @@ var ( func TestConvertDNSConfigAll(t *testing.T) { dnsConfig := convertDNSConfig(nameservers, search) assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ - Nameservers: nameservers, + Nameservers: toNetipAddrSlice(nameservers), Search: search, }, dnsConfig)) } @@ -315,7 +315,7 @@ func TestConvertDNSConfigAll(t *testing.T) { func TestConvertDNSConfigNameservers(t *testing.T) { dnsConfig := convertDNSConfig(nameservers, nil) assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ - Nameservers: nameservers, + Nameservers: toNetipAddrSlice(nameservers), Search: nil, }, dnsConfig)) } diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index 953bdd4f3d23..92fff4fa277c 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -25,7 +25,7 @@ import ( "github.com/docker/go-units" "github.com/go-viper/mapstructure/v2" "github.com/google/shlex" - "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/versions" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -925,7 +925,7 @@ func toServicePortConfigs(value string) ([]any, error) { return nil, err } // We need to sort the key of the ports to make sure it is consistent - keys := []string{} + keys := make([]string, 0, len(ports)) for port := range ports { keys = append(keys, string(port)) } @@ -933,7 +933,11 @@ func toServicePortConfigs(value string) ([]any, error) { for _, key := range keys { // Reuse ConvertPortToPortConfig so that it is consistent - portConfig, err := swarmopts.ConvertPortToPortConfig(container.PortRangeProto(key), portBindings) + port, err := network.ParsePort(key) + if err != nil { + return nil, err + } + portConfig, err := swarmopts.ConvertPortToPortConfig(port, portBindings) if err != nil { return nil, err } diff --git a/docs/reference/commandline/network_connect.md b/docs/reference/commandline/network_connect.md index 3a6514fab719..c3e373d5e867 100644 --- a/docs/reference/commandline/network_connect.md +++ b/docs/reference/commandline/network_connect.md @@ -10,10 +10,10 @@ Connect a container to a network | [`--alias`](#alias) | `stringSlice` | | Add network-scoped alias for the container | | `--driver-opt` | `stringSlice` | | driver options for the network | | `--gw-priority` | `int` | `0` | Highest gw-priority provides the default gateway. Accepts positive and negative values. | -| [`--ip`](#ip) | `string` | | IPv4 address (e.g., `172.30.100.104`) | -| `--ip6` | `string` | | IPv6 address (e.g., `2001:db8::33`) | +| [`--ip`](#ip) | `ip` | `` | IPv4 address (e.g., `172.30.100.104`) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., `2001:db8::33`) | | [`--link`](#link) | `list` | | Add link to another container | -| `--link-local-ip` | `stringSlice` | | Add a link-local address for the container | +| `--link-local-ip` | `ipSlice` | | Add a link-local address for the container | diff --git a/docs/reference/commandline/network_create.md b/docs/reference/commandline/network_create.md index 4c36adf9f487..ac493a844e10 100644 --- a/docs/reference/commandline/network_create.md +++ b/docs/reference/commandline/network_create.md @@ -12,10 +12,10 @@ Create a network | `--config-from` | `string` | | The network from which to copy the configuration | | `--config-only` | `bool` | | Create a configuration only network | | `-d`, `--driver` | `string` | `bridge` | Driver to manage the Network | -| `--gateway` | `stringSlice` | | IPv4 or IPv6 Gateway for the master subnet | +| `--gateway` | `ipSlice` | | IPv4 or IPv6 Gateway for the master subnet | | [`--ingress`](#ingress) | `bool` | | Create swarm routing-mesh network | | [`--internal`](#internal) | `bool` | | Restrict external access to the network | -| `--ip-range` | `stringSlice` | | Allocate container ip from a sub-range | +| `--ip-range` | `ipNetSlice` | | Allocate container ip from a sub-range | | `--ipam-driver` | `string` | `default` | IP Address Management Driver | | `--ipam-opt` | `map` | `map[]` | Set IPAM driver specific options | | `--ipv4` | `bool` | `true` | Enable or disable IPv4 address assignment | diff --git a/internal/test/builders/container.go b/internal/test/builders/container.go index 45e98682373f..43dbc5603141 100644 --- a/internal/test/builders/container.go +++ b/internal/test/builders/container.go @@ -1,6 +1,7 @@ package builders import ( + "net/netip" "time" "github.com/moby/moby/api/types/container" @@ -73,7 +74,7 @@ func WithSize(size int64) func(*container.Summary) { // IP sets the ip of the port func IP(ip string) func(*container.PortSummary) { return func(p *container.PortSummary) { - p.IP = ip + p.IP = netip.MustParseAddr(ip) } } diff --git a/opts/network.go b/opts/network.go index 43b3a09d4151..489ef8be3971 100644 --- a/opts/network.go +++ b/opts/network.go @@ -4,6 +4,7 @@ import ( "encoding/csv" "errors" "fmt" + "net/netip" "regexp" "strconv" "strings" @@ -26,9 +27,9 @@ type NetworkAttachmentOpts struct { Aliases []string DriverOpts map[string]string Links []string // TODO add support for links in the csv notation of `--network` - IPv4Address string - IPv6Address string - LinkLocalIPs []string + IPv4Address netip.Addr + IPv6Address netip.Addr + LinkLocalIPs []netip.Addr MacAddress string GwPriority int } @@ -70,13 +71,23 @@ func (n *NetworkOpt) Set(value string) error { //nolint:gocyclo case networkOptAlias: netOpt.Aliases = append(netOpt.Aliases, val) case networkOptIPv4Address: - netOpt.IPv4Address = val + netOpt.IPv4Address, err = netip.ParseAddr(val) + if err != nil { + return err + } case networkOptIPv6Address: - netOpt.IPv6Address = val + netOpt.IPv6Address, err = netip.ParseAddr(val) + if err != nil { + return err + } case networkOptMacAddress: netOpt.MacAddress = val case networkOptLinkLocalIP: - netOpt.LinkLocalIPs = append(netOpt.LinkLocalIPs, val) + a, err := netip.ParseAddr(val) + if err != nil { + return err + } + netOpt.LinkLocalIPs = append(netOpt.LinkLocalIPs, a) case driverOpt: key, val, err = parseDriverOpt(val) if err != nil { diff --git a/opts/network_test.go b/opts/network_test.go index ffdcbf26cd80..fba0920bc273 100644 --- a/opts/network_test.go +++ b/opts/network_test.go @@ -1,6 +1,7 @@ package opts import ( + "net/netip" "testing" "gotest.tools/v3/assert" @@ -64,8 +65,8 @@ func TestNetworkOptAdvancedSyntax(t *testing.T) { { Target: "docknet1", Aliases: []string{}, - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), }, }, }, @@ -94,7 +95,7 @@ func TestNetworkOptAdvancedSyntax(t *testing.T) { { Target: "docknet1", Aliases: []string{}, - LinkLocalIPs: []string{"169.254.169.254", "169.254.10.10"}, + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254"), netip.MustParseAddr("169.254.10.10")}, }, }, }, diff --git a/opts/swarmopts/port.go b/opts/swarmopts/port.go index cab64f0ab52d..f05fe92e40e7 100644 --- a/opts/swarmopts/port.go +++ b/opts/swarmopts/port.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/docker/go-connections/nat" - "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/swarm" "github.com/sirupsen/logrus" ) @@ -43,7 +43,7 @@ func (p *PortOpt) Set(value string) error { } pConfig := swarm.PortConfig{ - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, PublishMode: swarm.PortConfigPublishModeIngress, } for _, field := range fields { @@ -54,9 +54,9 @@ func (p *PortOpt) Set(value string) error { } switch key { case portOptProtocol: - switch swarm.PortConfigProtocol(val) { - case swarm.PortConfigProtocolTCP, swarm.PortConfigProtocolUDP, swarm.PortConfigProtocolSCTP: - pConfig.Protocol = swarm.PortConfigProtocol(val) + switch proto := network.IPProtocol(val); proto { + case network.TCP, network.UDP, network.SCTP: + pConfig.Protocol = proto default: return fmt.Errorf("invalid protocol value '%s'", val) } @@ -115,7 +115,11 @@ func (p *PortOpt) Set(value string) error { var portConfigs []swarm.PortConfig for port := range ports { - portConfig, err := ConvertPortToPortConfig(port, portBindingMap) + portProto, err := network.ParsePort(string(port)) + if err != nil { + return err + } + portConfig, err := ConvertPortToPortConfig(portProto, portBindingMap) if err != nil { return err } @@ -148,30 +152,29 @@ func (p *PortOpt) Value() []swarm.PortConfig { // ConvertPortToPortConfig converts ports to the swarm type func ConvertPortToPortConfig( - portRangeProto container.PortRangeProto, - portBindings map[container.PortRangeProto][]container.PortBinding, + portProto network.Port, + portBindings map[nat.Port][]nat.PortBinding, ) ([]swarm.PortConfig, error) { - proto, port := nat.SplitProtoPort(string(portRangeProto)) - portInt, _ := strconv.ParseUint(port, 10, 16) - proto = strings.ToLower(proto) + portProto.Num() + proto, portInt := portProto.Proto(), portProto.Num() ports := make([]swarm.PortConfig, 0, len(portBindings)) - for _, binding := range portBindings[portRangeProto] { + for _, binding := range portBindings[nat.Port(portProto.String())] { if p := net.ParseIP(binding.HostIP); p != nil && !p.IsUnspecified() { // TODO(thaJeztah): use context-logger, so that this output can be suppressed (in tests). - logrus.Warnf("ignoring IP-address (%s:%s) service will listen on '0.0.0.0'", net.JoinHostPort(binding.HostIP, binding.HostPort), portRangeProto) + logrus.Warnf("ignoring IP-address (%s:%s) service will listen on '0.0.0.0'", net.JoinHostPort(binding.HostIP, binding.HostPort), portProto) } startHostPort, endHostPort, err := nat.ParsePortRange(binding.HostPort) if err != nil && binding.HostPort != "" { - return nil, fmt.Errorf("invalid hostport binding (%s) for port (%s)", binding.HostPort, port) + return nil, fmt.Errorf("invalid hostport binding (%s) for port (%d)", binding.HostPort, portProto.Num()) } for i := startHostPort; i <= endHostPort; i++ { ports = append(ports, swarm.PortConfig{ // TODO Name: ? - Protocol: swarm.PortConfigProtocol(proto), + Protocol: proto, TargetPort: uint32(portInt), PublishedPort: uint32(i), PublishMode: swarm.PortConfigPublishModeIngress, diff --git a/opts/swarmopts/port_test.go b/opts/swarmopts/port_test.go index ad1838b60a91..979f6802afa9 100644 --- a/opts/swarmopts/port_test.go +++ b/opts/swarmopts/port_test.go @@ -5,7 +5,8 @@ import ( "os" "testing" - "github.com/moby/moby/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/swarm" "github.com/sirupsen/logrus" "gotest.tools/v3/assert" @@ -348,7 +349,7 @@ func TestConvertPortToPortConfigWithIP(t *testing.T) { logrus.SetOutput(&b) for _, tc := range testCases { t.Run(tc.value, func(t *testing.T) { - _, err := ConvertPortToPortConfig("80/tcp", map[container.PortRangeProto][]container.PortBinding{ + _, err := ConvertPortToPortConfig(network.MustParsePort("80/tcp"), map[nat.Port][]nat.PortBinding{ "80/tcp": {{HostIP: tc.value, HostPort: "2345"}}, }) assert.NilError(t, err) diff --git a/vendor.mod b/vendor.mod index c7084ef208c3..010e2505bebe 100644 --- a/vendor.mod +++ b/vendor.mod @@ -28,8 +28,8 @@ require ( github.com/google/uuid v1.6.0 github.com/mattn/go-runewidth v0.0.17 github.com/moby/go-archive v0.1.0 - github.com/moby/moby/api v1.52.0-beta.1.0.20250930082920-4ca8aedf929f // master - github.com/moby/moby/client v0.1.0-beta.0.0.20250930082920-4ca8aedf929f // master + github.com/moby/moby/api v1.52.0-beta.1.0.20251006143509-694e30abff1c // master + github.com/moby/moby/client v0.1.0-beta.0.0.20251006143509-694e30abff1c // master github.com/moby/patternmatcher v0.6.0 github.com/moby/swarmkit/v2 v2.1.0 github.com/moby/sys/atomicwriter v0.1.0 diff --git a/vendor.sum b/vendor.sum index e0a6e9613cbf..38121f6dea99 100644 --- a/vendor.sum +++ b/vendor.sum @@ -170,10 +170,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/moby/api v1.52.0-beta.1.0.20250930082920-4ca8aedf929f h1:2KUOgolCU2LE+IbKEyrFK478EwZiI4mpeKO8nvztkno= -github.com/moby/moby/api v1.52.0-beta.1.0.20250930082920-4ca8aedf929f/go.mod h1:8sBV0soUREiudtow4vqJGOxa4GyHI5vLQmvgKdHq5Ok= -github.com/moby/moby/client v0.1.0-beta.0.0.20250930082920-4ca8aedf929f h1:H28/bSN4nOtXzD3iBu7SOMGNZh4FTEpFiZMNLWyiQDI= -github.com/moby/moby/client v0.1.0-beta.0.0.20250930082920-4ca8aedf929f/go.mod h1:o5CkJu0RlmnLWRZRaEd7fL6wo0Ggr8Hw/UvgqdIUBuI= +github.com/moby/moby/api v1.52.0-beta.1.0.20251006143509-694e30abff1c h1:YmtLrQLg3cQIO0BXojmzK86k683yz01fjSgiNKWKX98= +github.com/moby/moby/api v1.52.0-beta.1.0.20251006143509-694e30abff1c/go.mod h1:/ou52HkRydg4+odrUR3vFsGgjIyHvprrpEQEkweL10s= +github.com/moby/moby/client v0.1.0-beta.0.0.20251006143509-694e30abff1c h1:r6qSAHeiGgN3kEHCF+fTu4kIMMITXZNk3KKN6JCo1oM= +github.com/moby/moby/client v0.1.0-beta.0.0.20251006143509-694e30abff1c/go.mod h1:o5CkJu0RlmnLWRZRaEd7fL6wo0Ggr8Hw/UvgqdIUBuI= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/swarmkit/v2 v2.1.0 h1:u+cJ5hSyF3HnzsyI+NtegYxdIPQIuibk7IbpXNxuISM= diff --git a/vendor/github.com/moby/moby/api/types/container/config.go b/vendor/github.com/moby/moby/api/types/container/config.go index 12fa6b310233..c5650f10c9c0 100644 --- a/vendor/github.com/moby/moby/api/types/container/config.go +++ b/vendor/github.com/moby/moby/api/types/container/config.go @@ -4,6 +4,7 @@ import ( "time" dockerspec "github.com/moby/docker-image-spec/specs-go/v1" + "github.com/moby/moby/api/types/network" ) // MinimumDuration puts a minimum on user configured duration. @@ -28,7 +29,7 @@ type Config struct { AttachStdin bool // Attach the standard input, makes possible user interaction AttachStdout bool // Attach the standard output AttachStderr bool // Attach the standard error - ExposedPorts PortSet `json:",omitempty"` // List of exposed ports + ExposedPorts network.PortSet `json:",omitempty"` // List of exposed ports Tty bool // Attach standard streams to a tty, including stdin if it is not closed. OpenStdin bool // Open stdin StdinOnce bool // If true, close stdin after the 1 attached client disconnects. diff --git a/vendor/github.com/moby/moby/api/types/container/hostconfig.go b/vendor/github.com/moby/moby/api/types/container/hostconfig.go index f167d805990a..0f889c65124c 100644 --- a/vendor/github.com/moby/moby/api/types/container/hostconfig.go +++ b/vendor/github.com/moby/moby/api/types/container/hostconfig.go @@ -3,6 +3,7 @@ package container import ( "errors" "fmt" + "net/netip" "strings" "github.com/docker/go-units" @@ -420,7 +421,7 @@ type HostConfig struct { ContainerIDFile string // File (path) where the containerId is written LogConfig LogConfig // Configuration of the logs for this container NetworkMode NetworkMode // Network mode to use for the container - PortBindings PortMap // Port mapping between the exposed port (container) and the host + PortBindings network.PortMap // Port mapping between the exposed port (container) and the host RestartPolicy RestartPolicy // Restart policy to be used for the container AutoRemove bool // Automatically remove container when it exits VolumeDriver string // Name of the volume driver used to mount volumes @@ -432,7 +433,7 @@ type HostConfig struct { CapAdd []string // List of kernel capabilities to add to the container CapDrop []string // List of kernel capabilities to remove from the container CgroupnsMode CgroupnsMode // Cgroup namespace mode to use for the container - DNS []string `json:"Dns"` // List of DNS server to lookup + DNS []netip.Addr `json:"Dns"` // List of DNS server to lookup DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for ExtraHosts []string // List of extra hosts diff --git a/vendor/github.com/moby/moby/api/types/container/nat_aliases.go b/vendor/github.com/moby/moby/api/types/container/nat_aliases.go deleted file mode 100644 index 470f15655cc3..000000000000 --- a/vendor/github.com/moby/moby/api/types/container/nat_aliases.go +++ /dev/null @@ -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 diff --git a/vendor/github.com/moby/moby/api/types/container/network_settings.go b/vendor/github.com/moby/moby/api/types/container/network_settings.go index fb7f59df087e..c51c0839d237 100644 --- a/vendor/github.com/moby/moby/api/types/container/network_settings.go +++ b/vendor/github.com/moby/moby/api/types/container/network_settings.go @@ -6,10 +6,13 @@ import ( // NetworkSettings exposes the network settings in the api type NetworkSettings struct { - SandboxID string // SandboxID uniquely represents a container's network stack - SandboxKey string // SandboxKey identifies the sandbox - Ports PortMap // Ports is a collection of PortBinding indexed by Port - Networks map[string]*network.EndpointSettings + SandboxID string // SandboxID uniquely represents a container's network stack + SandboxKey string // SandboxKey identifies the sandbox + + // Ports is a collection of [network.PortBinding] indexed by [network.Port] + Ports network.PortMap + + Networks map[string]*network.EndpointSettings } // NetworkSettingsSummary provides a summary of container's networks diff --git a/vendor/github.com/moby/moby/api/types/container/port_summary.go b/vendor/github.com/moby/moby/api/types/container/port_summary.go index 3956224dcc6e..68148eece462 100644 --- a/vendor/github.com/moby/moby/api/types/container/port_summary.go +++ b/vendor/github.com/moby/moby/api/types/container/port_summary.go @@ -5,6 +5,10 @@ package container // This file was generated by the swagger tool. // Editing this file might prove futile when you re-run the swagger generate command +import ( + "net/netip" +) + // PortSummary Describes a port-mapping between the container and the host. // // Example: {"PrivatePort":8080,"PublicPort":80,"Type":"tcp"} @@ -13,7 +17,7 @@ package container type PortSummary struct { // Host IP address that the container's port is mapped to - IP string `json:"IP,omitempty"` + IP netip.Addr `json:"IP,omitempty"` // Port on the container // Required: true diff --git a/vendor/github.com/moby/moby/api/types/image/image_inspect.go b/vendor/github.com/moby/moby/api/types/image/image_inspect.go index 69a263756356..baded4945701 100644 --- a/vendor/github.com/moby/moby/api/types/image/image_inspect.go +++ b/vendor/github.com/moby/moby/api/types/image/image_inspect.go @@ -47,12 +47,14 @@ type InspectResponse struct { // // Depending on how the image was created, this field may be empty and // is only set for images that were built/created locally. This field - // is empty if the image was pulled from an image registry. - Parent string + // is omitted if the image was pulled from an image registry. + // + // Deprecated: this field is deprecated, and will be removed in the next release. + Parent string `json:",omitempty"` // Comment is an optional message that can be set when committing or - // importing the image. - Comment string + // importing the image. This field is omitted if not set. + Comment string `json:",omitempty"` // Created is the date and time at which the image was created, formatted in // RFC 3339 nano-seconds (time.RFC3339Nano). @@ -79,12 +81,15 @@ type InspectResponse struct { // DockerVersion is the version of Docker that was used to build the image. // - // Depending on how the image was created, this field may be empty. - DockerVersion string + // Depending on how the image was created, this field may be omitted. + // + // Deprecated: this field is deprecated, and will be removed in the next release. + DockerVersion string `json:",omitempty"` // Author is the name of the author that was specified when committing the // image, or as specified through MAINTAINER (deprecated) in the Dockerfile. - Author string + // This field is omitted if not set. + Author string `json:",omitempty"` Config *dockerspec.DockerOCIImageConfig // Architecture is the hardware CPU architecture that the image runs on. diff --git a/vendor/github.com/moby/moby/api/types/network/endpoint.go b/vendor/github.com/moby/moby/api/types/network/endpoint.go index 39b732e0e7e2..ee5223d052a7 100644 --- a/vendor/github.com/moby/moby/api/types/network/endpoint.go +++ b/vendor/github.com/moby/moby/api/types/network/endpoint.go @@ -1,10 +1,8 @@ package network import ( - "errors" - "fmt" "maps" - "net" + "net/netip" "slices" ) @@ -28,11 +26,11 @@ type EndpointSettings struct { // Operational data NetworkID string EndpointID string - Gateway string - IPAddress string + Gateway netip.Addr + IPAddress netip.Addr IPPrefixLen int - IPv6Gateway string - GlobalIPv6Address string + IPv6Gateway netip.Addr + GlobalIPv6Address netip.Addr GlobalIPv6PrefixLen int // DNSNames holds all the (non fully qualified) DNS names associated to this endpoint. First entry is used to // generate PTR records. @@ -57,9 +55,9 @@ func (es *EndpointSettings) Copy() *EndpointSettings { // EndpointIPAMConfig represents IPAM configurations for the endpoint type EndpointIPAMConfig struct { - IPv4Address string `json:",omitempty"` - IPv6Address string `json:",omitempty"` - LinkLocalIPs []string `json:",omitempty"` + IPv4Address netip.Addr `json:",omitempty"` + IPv6Address netip.Addr `json:",omitempty"` + LinkLocalIPs []netip.Addr `json:",omitempty"` } // Copy makes a copy of the endpoint ipam config @@ -71,76 +69,3 @@ func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig { cfgCopy.LinkLocalIPs = slices.Clone(cfg.LinkLocalIPs) return &cfgCopy } - -// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an -// EndpointIPAMConfig is valid for a specific network. -type NetworkSubnet interface { - // Contains checks whether the NetworkSubnet contains [addr]. - Contains(addr net.IP) bool - // IsStatic checks whether the subnet was statically allocated (ie. user-defined). - IsStatic() bool -} - -// IsInRange checks whether static IP addresses are valid in a specific network. -func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error { - var errs []error - - if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil { - errs = append(errs, err) - } - if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil { - errs = append(errs, err) - } - - return errJoin(errs...) -} - -func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error { - if epAddr == "" { - return nil - } - - var staticSubnet bool - parsedAddr := net.ParseIP(epAddr) - for _, subnet := range ipamSubnets { - if subnet.IsStatic() { - staticSubnet = true - if subnet.Contains(parsedAddr) { - return nil - } - } - } - - if staticSubnet { - return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr) - } - - return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets") -} - -// Validate checks whether cfg is valid. -func (cfg *EndpointIPAMConfig) Validate() error { - if cfg == nil { - return nil - } - - var errs []error - - if cfg.IPv4Address != "" { - if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() { - errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address)) - } - } - if cfg.IPv6Address != "" { - if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() { - errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address)) - } - } - for _, addr := range cfg.LinkLocalIPs { - if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() { - errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr)) - } - } - - return errJoin(errs...) -} diff --git a/vendor/github.com/moby/moby/api/types/network/endpoint_resource.go b/vendor/github.com/moby/moby/api/types/network/endpoint_resource.go index 9780253baf6d..6ff25b1bb6c0 100644 --- a/vendor/github.com/moby/moby/api/types/network/endpoint_resource.go +++ b/vendor/github.com/moby/moby/api/types/network/endpoint_resource.go @@ -5,6 +5,10 @@ package network // This file was generated by the swagger tool. // Editing this file might prove futile when you re-run the swagger generate command +import ( + "net/netip" +) + // EndpointResource contains network resources allocated and used for a container in a network. // // swagger:model EndpointResource @@ -24,8 +28,8 @@ type EndpointResource struct { // IPv4 address // Example: 172.19.0.2/16 - IPv4Address string `json:"IPv4Address"` + IPv4Address netip.Prefix `json:"IPv4Address"` // IPv6 address - IPv6Address string `json:"IPv6Address"` + IPv6Address netip.Prefix `json:"IPv6Address"` } diff --git a/vendor/github.com/moby/moby/api/types/network/ipam.go b/vendor/github.com/moby/moby/api/types/network/ipam.go index 2421d79654dc..e57be481b72c 100644 --- a/vendor/github.com/moby/moby/api/types/network/ipam.go +++ b/vendor/github.com/moby/moby/api/types/network/ipam.go @@ -1,10 +1,7 @@ package network import ( - "errors" - "fmt" "net/netip" - "strings" ) // IPAM represents IP Address Management @@ -16,160 +13,10 @@ type IPAM struct { // IPAMConfig represents IPAM configurations type IPAMConfig struct { - Subnet string `json:",omitempty"` - IPRange string `json:",omitempty"` - Gateway string `json:",omitempty"` - AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"` + Subnet netip.Prefix `json:",omitempty"` + IPRange netip.Prefix `json:",omitempty"` + Gateway netip.Addr `json:",omitempty"` + AuxAddress map[string]netip.Addr `json:"AuxiliaryAddresses,omitempty"` } type SubnetStatuses = map[netip.Prefix]SubnetStatus - -type ipFamily string - -const ( - ip4 ipFamily = "IPv4" - ip6 ipFamily = "IPv6" -) - -// ValidateIPAM checks whether the network's IPAM passed as argument is valid. It returns a joinError of the list of -// errors found. -func ValidateIPAM(ipam *IPAM, enableIPv6 bool) error { - if ipam == nil { - return nil - } - - var errs []error - for _, cfg := range ipam.Config { - subnet, err := netip.ParsePrefix(cfg.Subnet) - if err != nil { - errs = append(errs, fmt.Errorf("invalid subnet %s: invalid CIDR block notation", cfg.Subnet)) - continue - } - subnetFamily := ip4 - if subnet.Addr().Is6() { - subnetFamily = ip6 - } - - if !enableIPv6 && subnetFamily == ip6 { - continue - } - - if subnet != subnet.Masked() { - errs = append(errs, fmt.Errorf("invalid subnet %s: it should be %s", subnet, subnet.Masked())) - } - - if ipRangeErrs := validateIPRange(cfg.IPRange, subnet, subnetFamily); len(ipRangeErrs) > 0 { - errs = append(errs, ipRangeErrs...) - } - - if err := validateAddress(cfg.Gateway, subnet, subnetFamily); err != nil { - errs = append(errs, fmt.Errorf("invalid gateway %s: %w", cfg.Gateway, err)) - } - - for auxName, aux := range cfg.AuxAddress { - if err := validateAddress(aux, subnet, subnetFamily); err != nil { - errs = append(errs, fmt.Errorf("invalid auxiliary address %s: %w", auxName, err)) - } - } - } - - if err := errJoin(errs...); err != nil { - return fmt.Errorf("invalid network config:\n%w", err) - } - - return nil -} - -func validateIPRange(ipRange string, subnet netip.Prefix, subnetFamily ipFamily) []error { - if ipRange == "" { - return nil - } - prefix, err := netip.ParsePrefix(ipRange) - if err != nil { - return []error{fmt.Errorf("invalid ip-range %s: invalid CIDR block notation", ipRange)} - } - family := ip4 - if prefix.Addr().Is6() { - family = ip6 - } - - if family != subnetFamily { - return []error{fmt.Errorf("invalid ip-range %s: parent subnet is an %s block", ipRange, subnetFamily)} - } - - var errs []error - if prefix.Bits() < subnet.Bits() { - errs = append(errs, fmt.Errorf("invalid ip-range %s: CIDR block is bigger than its parent subnet %s", ipRange, subnet)) - } - if prefix != prefix.Masked() { - errs = append(errs, fmt.Errorf("invalid ip-range %s: it should be %s", prefix, prefix.Masked())) - } - if !subnet.Overlaps(prefix) { - errs = append(errs, fmt.Errorf("invalid ip-range %s: parent subnet %s doesn't contain ip-range", ipRange, subnet)) - } - - return errs -} - -func validateAddress(address string, subnet netip.Prefix, subnetFamily ipFamily) error { - if address == "" { - return nil - } - addr, err := netip.ParseAddr(address) - if err != nil { - return errors.New("invalid address") - } - family := ip4 - if addr.Is6() { - family = ip6 - } - - if family != subnetFamily { - return fmt.Errorf("parent subnet is an %s block", subnetFamily) - } - if !subnet.Contains(addr) { - return fmt.Errorf("parent subnet %s doesn't contain this address", subnet) - } - - return nil -} - -func errJoin(errs ...error) error { - n := 0 - for _, err := range errs { - if err != nil { - n++ - } - } - if n == 0 { - return nil - } - e := &joinError{ - errs: make([]error, 0, n), - } - for _, err := range errs { - if err != nil { - e.errs = append(e.errs, err) - } - } - return e -} - -type joinError struct { - errs []error -} - -func (e *joinError) Error() string { - if len(e.errs) == 1 { - return strings.TrimSpace(e.errs[0].Error()) - } - stringErrs := make([]string, 0, len(e.errs)) - for _, subErr := range e.errs { - stringErrs = append(stringErrs, strings.ReplaceAll(subErr.Error(), "\n", "\n\t")) - } - return "* " + strings.Join(stringErrs, "\n* ") -} - -func (e *joinError) Unwrap() []error { - return e.errs -} diff --git a/vendor/github.com/moby/moby/api/types/network/network_types.go b/vendor/github.com/moby/moby/api/types/network/network_types.go index 413e5fbed051..5401f55f826c 100644 --- a/vendor/github.com/moby/moby/api/types/network/network_types.go +++ b/vendor/github.com/moby/moby/api/types/network/network_types.go @@ -30,14 +30,6 @@ type CreateRequest struct { Labels map[string]string // Labels holds metadata specific to the network being created. } -// ServiceInfo represents service parameters with the list of service's tasks -type ServiceInfo struct { - VIP string - Ports []string - LocalLBIndex int - Tasks []Task -} - // NetworkingConfig represents the container's networking configuration for each of its interfaces // Carries the networking configs specified in the `docker run` and `docker network connect` commands type NetworkingConfig struct { diff --git a/vendor/github.com/moby/moby/api/types/network/peer_info.go b/vendor/github.com/moby/moby/api/types/network/peer_info.go index 0522a2392d54..dc88ec16fa9b 100644 --- a/vendor/github.com/moby/moby/api/types/network/peer_info.go +++ b/vendor/github.com/moby/moby/api/types/network/peer_info.go @@ -5,6 +5,10 @@ package network // This file was generated by the swagger tool. // Editing this file might prove futile when you re-run the swagger generate command +import ( + "net/netip" +) + // PeerInfo represents one peer of an overlay network. // // swagger:model PeerInfo @@ -16,5 +20,5 @@ type PeerInfo struct { // IP-address of the peer-node in the Swarm cluster. // Example: 10.133.77.91 - IP string `json:"IP"` + IP netip.Addr `json:"IP"` } diff --git a/vendor/github.com/moby/moby/api/types/network/port.go b/vendor/github.com/moby/moby/api/types/network/port.go new file mode 100644 index 000000000000..171d9f51d35c --- /dev/null +++ b/vendor/github.com/moby/moby/api/types/network/port.go @@ -0,0 +1,346 @@ +package network + +import ( + "errors" + "fmt" + "iter" + "net/netip" + "strconv" + "strings" + "unique" +) + +// IPProtocol represents a network protocol for a port. +type IPProtocol string + +const ( + TCP IPProtocol = "tcp" + UDP IPProtocol = "udp" + SCTP IPProtocol = "sctp" +) + +// Sentinel port proto value for zero Port and PortRange values. +var protoZero unique.Handle[IPProtocol] + +// Port is a type representing a single port number and protocol in the format "/[]". +// +// 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[IPProtocol] +} + +// 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 IPProtocol) (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() IPProtocol { + 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 "/". +// 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 netip.Addr `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[IPProtocol] +} + +// 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 IPProtocol) (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() IPProtocol { + 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 "-/" or "/" 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 0–65535. +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[IPProtocol] { + if proto == "" { + return unique.Make(TCP) + } + + proto = strings.ToLower(proto) + + return unique.Make(IPProtocol(proto)) +} diff --git a/vendor/github.com/moby/moby/api/types/network/service_info.go b/vendor/github.com/moby/moby/api/types/network/service_info.go new file mode 100644 index 000000000000..fdd92f161151 --- /dev/null +++ b/vendor/github.com/moby/moby/api/types/network/service_info.go @@ -0,0 +1,28 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package network + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/netip" +) + +// ServiceInfo represents service parameters with the list of service's tasks +// +// swagger:model ServiceInfo +type ServiceInfo struct { + + // v IP + VIP netip.Addr `json:"VIP"` + + // ports + Ports []string `json:"Ports"` + + // local l b index + LocalLBIndex int `json:"LocalLBIndex"` + + // tasks + Tasks []Task `json:"Tasks"` +} diff --git a/vendor/github.com/moby/moby/api/types/network/task.go b/vendor/github.com/moby/moby/api/types/network/task.go index 9a55fa17373b..a547523a44ea 100644 --- a/vendor/github.com/moby/moby/api/types/network/task.go +++ b/vendor/github.com/moby/moby/api/types/network/task.go @@ -5,6 +5,10 @@ package network // This file was generated by the swagger tool. // Editing this file might prove futile when you re-run the swagger generate command +import ( + "net/netip" +) + // Task carries the information about one backend task // // swagger:model Task @@ -17,7 +21,7 @@ type Task struct { EndpointID string `json:"EndpointID"` // endpoint IP - EndpointIP string `json:"EndpointIP"` + EndpointIP netip.Addr `json:"EndpointIP"` // info Info map[string]string `json:"Info"` diff --git a/vendor/github.com/moby/moby/api/types/plugin/plugin.go b/vendor/github.com/moby/moby/api/types/plugin/plugin.go index b7c768d7fc66..e1adda629250 100644 --- a/vendor/github.com/moby/moby/api/types/plugin/plugin.go +++ b/vendor/github.com/moby/moby/api/types/plugin/plugin.go @@ -51,8 +51,11 @@ type Config struct { // Required: true Description string `json:"Description"` - // Docker Version used to create the plugin - // Example: 17.06.0-ce + // Docker Version used to create the plugin. + // + // Depending on how the plugin was created, this field may be empty or omitted. + // + // Deprecated: this field is no longer set, and will be removed in the next API version. DockerVersion string `json:"DockerVersion,omitempty"` // documentation diff --git a/vendor/github.com/moby/moby/api/types/registry/registry.go b/vendor/github.com/moby/moby/api/types/registry/registry.go index 97c396fe0c73..ef5a486ed6c7 100644 --- a/vendor/github.com/moby/moby/api/types/registry/registry.go +++ b/vendor/github.com/moby/moby/api/types/registry/registry.go @@ -1,15 +1,14 @@ package registry import ( - "encoding/json" - "net" + "net/netip" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // ServiceConfig stores daemon registry services configuration. type ServiceConfig struct { - InsecureRegistryCIDRs []*NetIPNet `json:"InsecureRegistryCIDRs"` + InsecureRegistryCIDRs []netip.Prefix `json:"InsecureRegistryCIDRs"` IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` Mirrors []string @@ -17,34 +16,6 @@ type ServiceConfig struct { ExtraFields map[string]any `json:"-"` } -// NetIPNet is the net.IPNet type, which can be marshalled and -// unmarshalled to JSON -type NetIPNet net.IPNet - -// String returns the CIDR notation of ipnet -func (ipnet *NetIPNet) String() string { - return (*net.IPNet)(ipnet).String() -} - -// MarshalJSON returns the JSON representation of the IPNet -func (ipnet *NetIPNet) MarshalJSON() ([]byte, error) { - return json.Marshal((*net.IPNet)(ipnet).String()) -} - -// UnmarshalJSON sets the IPNet from a byte array of JSON -func (ipnet *NetIPNet) UnmarshalJSON(b []byte) error { - var ipnetStr string - if err := json.Unmarshal(b, &ipnetStr); err != nil { - return err - } - _, cidr, err := net.ParseCIDR(ipnetStr) - if err != nil { - return err - } - *ipnet = NetIPNet(*cidr) - return nil -} - // IndexInfo contains information about a registry // // RepositoryInfo Examples: diff --git a/vendor/github.com/moby/moby/api/types/swarm/container.go b/vendor/github.com/moby/moby/api/types/swarm/container.go index bc8690d8cbb5..268565ec8ae4 100644 --- a/vendor/github.com/moby/moby/api/types/swarm/container.go +++ b/vendor/github.com/moby/moby/api/types/swarm/container.go @@ -1,6 +1,7 @@ package swarm import ( + "net/netip" "time" "github.com/moby/moby/api/types/container" @@ -14,7 +15,7 @@ import ( // TODO: `domain` is not supported yet. type DNSConfig struct { // Nameservers specifies the IP addresses of the name servers - Nameservers []string `json:",omitempty"` + Nameservers []netip.Addr `json:",omitempty"` // Search specifies the search list for host-name lookup Search []string `json:",omitempty"` // Options allows certain internal resolver variables to be modified diff --git a/vendor/github.com/moby/moby/api/types/swarm/network.go b/vendor/github.com/moby/moby/api/types/swarm/network.go index 95a5fb385a51..5e2517174f48 100644 --- a/vendor/github.com/moby/moby/api/types/swarm/network.go +++ b/vendor/github.com/moby/moby/api/types/swarm/network.go @@ -1,6 +1,8 @@ package swarm import ( + "net/netip" + "github.com/moby/moby/api/types/network" ) @@ -30,7 +32,7 @@ const ( // PortConfig represents the config of a port. type PortConfig struct { Name string `json:",omitempty"` - Protocol PortConfigProtocol `json:",omitempty"` + Protocol network.IPProtocol `json:",omitempty"` // TargetPort is the port inside the container TargetPort uint32 `json:",omitempty"` // PublishedPort is the port on the swarm hosts @@ -52,24 +54,10 @@ const ( PortConfigPublishModeHost PortConfigPublishMode = "host" ) -// PortConfigProtocol represents the protocol of a port. -type PortConfigProtocol string - -const ( - // TODO(stevvooe): These should be used generally, not just for PortConfig. - - // PortConfigProtocolTCP TCP - PortConfigProtocolTCP PortConfigProtocol = "tcp" - // PortConfigProtocolUDP UDP - PortConfigProtocolUDP PortConfigProtocol = "udp" - // PortConfigProtocolSCTP SCTP - PortConfigProtocolSCTP PortConfigProtocol = "sctp" -) - // EndpointVirtualIP represents the virtual ip of a port. type EndpointVirtualIP struct { - NetworkID string `json:",omitempty"` - Addr string `json:",omitempty"` + NetworkID string `json:",omitempty"` + Addr netip.Addr `json:",omitempty"` } // Network represents a network. @@ -103,8 +91,8 @@ type NetworkAttachmentConfig struct { // NetworkAttachment represents a network attachment. type NetworkAttachment struct { - Network Network `json:",omitempty"` - Addresses []string `json:",omitempty"` + Network Network `json:",omitempty"` + Addresses []netip.Addr `json:",omitempty"` } // IPAMOptions represents ipam options. @@ -115,7 +103,7 @@ type IPAMOptions struct { // IPAMConfig represents ipam configuration. type IPAMConfig struct { - Subnet string `json:",omitempty"` - Range string `json:",omitempty"` - Gateway string `json:",omitempty"` + Subnet netip.Prefix `json:",omitempty"` + Range netip.Prefix `json:",omitempty"` + Gateway netip.Addr `json:",omitempty"` } diff --git a/vendor/github.com/moby/moby/api/types/swarm/swarm.go b/vendor/github.com/moby/moby/api/types/swarm/swarm.go index 4c37a982d170..7d683b30ae64 100644 --- a/vendor/github.com/moby/moby/api/types/swarm/swarm.go +++ b/vendor/github.com/moby/moby/api/types/swarm/swarm.go @@ -1,6 +1,7 @@ package swarm import ( + "net/netip" "time" ) @@ -12,7 +13,7 @@ type ClusterInfo struct { Spec Spec TLSInfo TLSInfo RootRotationInProgress bool - DefaultAddrPool []string + DefaultAddrPool []netip.Prefix SubnetSize uint32 DataPathPort uint32 } @@ -159,7 +160,7 @@ type InitRequest struct { Spec Spec AutoLockManagers bool Availability NodeAvailability - DefaultAddrPool []string + DefaultAddrPool []netip.Prefix SubnetSize uint32 } diff --git a/vendor/github.com/moby/moby/api/types/system/info.go b/vendor/github.com/moby/moby/api/types/system/info.go index 4bbc8c8aba6e..0147b42ee526 100644 --- a/vendor/github.com/moby/moby/api/types/system/info.go +++ b/vendor/github.com/moby/moby/api/types/system/info.go @@ -1,6 +1,8 @@ package system import ( + "net/netip" + "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/swarm" @@ -146,7 +148,7 @@ type Commit struct { // NetworkAddressPool is a temp struct used by [Info] struct. type NetworkAddressPool struct { - Base string + Base netip.Prefix Size int } diff --git a/vendor/github.com/moby/moby/api/types/types.go b/vendor/github.com/moby/moby/api/types/types.go index 3d00f354b6fe..32fbcc639fd7 100644 --- a/vendor/github.com/moby/moby/api/types/types.go +++ b/vendor/github.com/moby/moby/api/types/types.go @@ -11,6 +11,15 @@ const ( // MediaTypeMultiplexedStream is vendor specific MIME-Type set for stdin/stdout/stderr multiplexed streams MediaTypeMultiplexedStream = "application/vnd.docker.multiplexed-stream" + + // MediaTypeJSON is the MIME-Type for JSON objects + MediaTypeJSON = "application/json" + + // MediaTypeNDJson is the MIME-Type for Newline Delimited JSON objects streams + MediaTypeNDJSON = "application/x-ndjson" + + // MediaTypeJsonSequence is the MIME-Type for JSON Text Sequences (RFC7464) + MediaTypeJSONSequence = "application/json-seq" ) // Ping contains response of Engine API: diff --git a/vendor/github.com/moby/moby/client/build_cancel.go b/vendor/github.com/moby/moby/client/build_cancel.go index f39b8761611f..c9bea9d0068e 100644 --- a/vendor/github.com/moby/moby/client/build_cancel.go +++ b/vendor/github.com/moby/moby/client/build_cancel.go @@ -5,9 +5,11 @@ import ( "net/url" ) +type BuildCancelOptions struct{} + // BuildCancel requests the daemon to cancel the ongoing build request // with the given id. -func (cli *Client) BuildCancel(ctx context.Context, id string) error { +func (cli *Client) BuildCancel(ctx context.Context, id string, _ BuildCancelOptions) error { query := url.Values{} query.Set("id", id) diff --git a/vendor/github.com/moby/moby/client/build_prune.go b/vendor/github.com/moby/moby/client/build_prune.go index c7cbf076e865..9ea3bb6a77d5 100644 --- a/vendor/github.com/moby/moby/client/build_prune.go +++ b/vendor/github.com/moby/moby/client/build_prune.go @@ -21,8 +21,14 @@ type BuildCachePruneOptions struct { Filters filters.Args } +// BuildCachePruneResult holds the result from the BuildCachePrune method. +type BuildCachePruneResult struct { + Report build.CachePruneReport +} + // BuildCachePrune requests the daemon to delete unused cache data. -func (cli *Client) BuildCachePrune(ctx context.Context, opts BuildCachePruneOptions) (*build.CachePruneReport, error) { +func (cli *Client) BuildCachePrune(ctx context.Context, opts BuildCachePruneOptions) (BuildCachePruneResult, error) { + var out BuildCachePruneResult query := url.Values{} if opts.All { query.Set("all", "1") @@ -45,7 +51,7 @@ func (cli *Client) BuildCachePrune(ctx context.Context, opts BuildCachePruneOpti } f, err := filters.ToJSON(opts.Filters) if err != nil { - return nil, fmt.Errorf("prune could not marshal filters option: %w", err) + return BuildCachePruneResult{}, fmt.Errorf("prune could not marshal filters option: %w", err) } query.Set("filters", f) @@ -53,13 +59,14 @@ func (cli *Client) BuildCachePrune(ctx context.Context, opts BuildCachePruneOpti defer ensureReaderClosed(resp) if err != nil { - return nil, err + return BuildCachePruneResult{}, err } report := build.CachePruneReport{} if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { - return nil, fmt.Errorf("error retrieving disk usage: %w", err) + return BuildCachePruneResult{}, fmt.Errorf("error retrieving disk usage: %w", err) } - return &report, nil + out.Report = report + return out, nil } diff --git a/vendor/github.com/moby/moby/client/client_interfaces.go b/vendor/github.com/moby/moby/client/client_interfaces.go index 2f2fd3ce3df0..70a7e2331ef3 100644 --- a/vendor/github.com/moby/moby/client/client_interfaces.go +++ b/vendor/github.com/moby/moby/client/client_interfaces.go @@ -6,7 +6,6 @@ import ( "net" "github.com/moby/moby/api/types" - "github.com/moby/moby/api/types/build" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/events" "github.com/moby/moby/api/types/filters" @@ -109,8 +108,8 @@ type DistributionAPIClient interface { // ImageAPIClient defines API client methods for the images type ImageAPIClient interface { ImageBuild(ctx context.Context, context io.Reader, options ImageBuildOptions) (ImageBuildResponse, error) - BuildCachePrune(ctx context.Context, opts BuildCachePruneOptions) (*build.CachePruneReport, error) - BuildCancel(ctx context.Context, id string) error + BuildCachePrune(ctx context.Context, opts BuildCachePruneOptions) (BuildCachePruneResult, error) + BuildCancel(ctx context.Context, id string, opts BuildCancelOptions) error ImageCreate(ctx context.Context, parentReference string, options ImageCreateOptions) (io.ReadCloser, error) ImageImport(ctx context.Context, source ImageImportSource, ref string, options ImageImportOptions) (io.ReadCloser, error) diff --git a/vendor/github.com/moby/moby/client/container_create.go b/vendor/github.com/moby/moby/client/container_create.go index b15a5e65157c..89e4306e393c 100644 --- a/vendor/github.com/moby/moby/client/container_create.go +++ b/vendor/github.com/moby/moby/client/container_create.go @@ -48,10 +48,6 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config } if hostConfig != nil { - if versions.LessThan(cli.ClientVersion(), "1.25") { - // When using API 1.24 and under, the client is responsible for removing the container - hostConfig.AutoRemove = false - } if platform != nil && platform.OS == "linux" && versions.LessThan(cli.ClientVersion(), "1.42") { // When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize hostConfig.ConsoleSize = [2]uint{0, 0} diff --git a/vendor/github.com/moby/moby/client/internal/json-stream.go b/vendor/github.com/moby/moby/client/internal/json-stream.go new file mode 100644 index 000000000000..552978f9a183 --- /dev/null +++ b/vendor/github.com/moby/moby/client/internal/json-stream.go @@ -0,0 +1,50 @@ +package internal + +import ( + "encoding/json" + "io" + "slices" + + "github.com/moby/moby/api/types" +) + +const rs = 0x1E + +type DecoderFn func(v any) error + +// NewJSONStreamDecoder builds adequate DecoderFn to read json records formatted with specified content-type +func NewJSONStreamDecoder(r io.Reader, contentType string) DecoderFn { + switch contentType { + case types.MediaTypeJSONSequence: + return json.NewDecoder(NewRSFilterReader(r)).Decode + case types.MediaTypeJSON, types.MediaTypeNDJSON: + fallthrough + default: + return json.NewDecoder(r).Decode + } +} + +// RSFilterReader wraps an io.Reader and filters out ASCII RS characters +type RSFilterReader struct { + reader io.Reader + buffer []byte +} + +// NewRSFilterReader creates a new RSFilterReader that filters out RS characters +func NewRSFilterReader(r io.Reader) *RSFilterReader { + return &RSFilterReader{ + reader: r, + buffer: make([]byte, 4096), // Internal buffer for reading chunks + } +} + +// Read implements the io.Reader interface, filtering out RS characters +func (r *RSFilterReader) Read(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + + n, err = r.reader.Read(p) + filtered := slices.DeleteFunc(p[:n], func(b byte) bool { return b == rs }) + return len(filtered), err +} diff --git a/vendor/github.com/moby/moby/client/system_events.go b/vendor/github.com/moby/moby/client/system_events.go index 93b12cdefd8b..bee7402b5938 100644 --- a/vendor/github.com/moby/moby/client/system_events.go +++ b/vendor/github.com/moby/moby/client/system_events.go @@ -2,12 +2,14 @@ package client import ( "context" - "encoding/json" + "net/http" "net/url" "time" + "github.com/moby/moby/api/types" "github.com/moby/moby/api/types/events" "github.com/moby/moby/api/types/filters" + "github.com/moby/moby/client/internal" "github.com/moby/moby/client/internal/timestamp" ) @@ -37,7 +39,10 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-cha return } - resp, err := cli.get(ctx, "/events", query, nil) + headers := http.Header{} + headers.Add("Accept", types.MediaTypeJSONSequence) + headers.Add("Accept", types.MediaTypeNDJSON) + resp, err := cli.get(ctx, "/events", query, headers) if err != nil { close(started) errs <- err @@ -45,7 +50,8 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-cha } defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) + contentType := resp.Header.Get("Content-Type") + decoder := internal.NewJSONStreamDecoder(resp.Body, contentType) close(started) for { @@ -55,7 +61,7 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-cha return default: var event events.Message - if err := decoder.Decode(&event); err != nil { + if err := decoder(&event); err != nil { errs <- err return } diff --git a/vendor/modules.txt b/vendor/modules.txt index db9b74444ca5..f99bd4affbda 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -168,7 +168,7 @@ github.com/moby/docker-image-spec/specs-go/v1 github.com/moby/go-archive github.com/moby/go-archive/compression github.com/moby/go-archive/tarheader -# github.com/moby/moby/api v1.52.0-beta.1.0.20250930082920-4ca8aedf929f +# github.com/moby/moby/api v1.52.0-beta.1.0.20251006143509-694e30abff1c ## explicit; go 1.23.0 github.com/moby/moby/api/pkg/authconfig github.com/moby/moby/api/pkg/progress @@ -194,9 +194,10 @@ github.com/moby/moby/api/types/swarm github.com/moby/moby/api/types/system github.com/moby/moby/api/types/versions github.com/moby/moby/api/types/volume -# github.com/moby/moby/client v0.1.0-beta.0.0.20250930082920-4ca8aedf929f +# github.com/moby/moby/client v0.1.0-beta.0.0.20251006143509-694e30abff1c ## explicit; go 1.23.0 github.com/moby/moby/client +github.com/moby/moby/client/internal github.com/moby/moby/client/internal/timestamp github.com/moby/moby/client/pkg/jsonmessage github.com/moby/moby/client/pkg/security