vendor: github.com/moby/moby/api, client 0769fe708773 (master)

full diff: 4ca8aedf92...0769fe7087

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-10-06 12:54:54 +02:00
parent 6ddff81bee
commit f81816ef88
175 changed files with 1514 additions and 1830 deletions

View File

@ -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
}

View File

@ -70,8 +70,7 @@ const (
)
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
warning := normalWarning
if options.all {
@ -87,7 +86,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 +94,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")

View File

@ -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)

View File

@ -6,9 +6,7 @@ import (
"sort"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/volume"
@ -33,7 +31,7 @@ type fakeClient struct {
containerListFunc func(options client.ContainerListOptions) ([]container.Summary, error)
imageListFunc func(options client.ImageListOptions) ([]image.Summary, error)
networkListFunc func(ctx context.Context, options client.NetworkListOptions) ([]network.Summary, error)
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
volumeListFunc func(filter client.Filters) (volume.ListResponse, error)
}
func (c *fakeClient) ContainerList(_ context.Context, options client.ContainerListOptions) ([]container.Summary, error) {
@ -156,7 +154,7 @@ func TestCompleteContainerNames(t *testing.T) {
}
comp := ContainerNames(fakeCLI{&fakeClient{
containerListFunc: func(opts client.ContainerListOptions) ([]container.Summary, error) {
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(client.ContainerListOptions{}, filters.Args{})))
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
if tc.expDirective == cobra.ShellCompDirectiveError {
return nil, errors.New("some error occurred")
}
@ -339,7 +337,7 @@ func TestCompleteVolumeNames(t *testing.T) {
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := VolumeNames(fakeCLI{&fakeClient{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
volumeListFunc: func(filter client.Filters) (volume.ListResponse, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return volume.ListResponse{}, errors.New("some error occurred")
}

View File

@ -13,7 +13,6 @@ import (
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
@ -133,8 +132,8 @@ func TestConfigListWithFormat(t *testing.T) {
func TestConfigListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) ([]swarm.Config, error) {
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
assert.Check(t, options.Filters["name"]["foo"])
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"])
return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"),
builders.ConfigName("foo"),

View File

@ -5,7 +5,6 @@ import (
"io"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
@ -36,7 +35,7 @@ type fakeClient struct {
containerRestartFunc func(ctx context.Context, containerID string, options client.ContainerStopOptions) error
containerStopFunc func(ctx context.Context, containerID string, options client.ContainerStopOptions) error
containerKillFunc func(ctx context.Context, containerID, signal string) error
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
containerPruneFunc func(ctx context.Context, pruneFilters client.Filters) (container.PruneReport, error)
containerAttachFunc func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.HijackedResponse, error)
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
containerRenameFunc func(ctx context.Context, oldName, newName string) error
@ -172,7 +171,7 @@ func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal stri
return nil
}
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters client.Filters) (container.PruneReport, error) {
if f.containerPruneFunc != nil {
return f.containerPruneFunc(ctx, pruneFilters)
}

View File

@ -26,7 +26,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
expectedAll bool
expectedSize bool
expectedLimit int
expectedFilters map[string]string
expectedFilters client.Filters
}{
{
psOpts: &psOptions{
@ -35,13 +35,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
last: 5,
filter: filters,
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
},
{
psOpts: &psOptions{
@ -50,10 +47,9 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
last: -1,
nLatest: true,
},
expectedAll: true,
expectedSize: true,
expectedLimit: 1,
expectedFilters: make(map[string]string),
expectedAll: true,
expectedSize: true,
expectedLimit: 1,
},
{
psOpts: &psOptions{
@ -64,13 +60,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
// With .Size, size should be true
format: "{{.Size}}",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
},
{
psOpts: &psOptions{
@ -81,13 +74,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
// With .Size, size should be true
format: "{{.Size}} {{.CreatedAt}} {{upper .Networks}}",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
},
{
psOpts: &psOptions{
@ -98,13 +88,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
// Without .Size, size should be false
format: "{{.CreatedAt}} {{.Networks}}",
},
expectedAll: true,
expectedSize: false,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
expectedAll: true,
expectedSize: false,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
},
}
@ -115,14 +102,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
assert.Check(t, is.Equal(c.expectedAll, options.All))
assert.Check(t, is.Equal(c.expectedSize, options.Size))
assert.Check(t, is.Equal(c.expectedLimit, options.Limit))
assert.Check(t, is.Equal(len(c.expectedFilters), options.Filters.Len()))
for k, v := range c.expectedFilters {
f := options.Filters
if !f.ExactMatch(k, v) {
t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k))
}
}
assert.Check(t, is.DeepEqual(c.expectedFilters, options.Filters))
}
}

View File

@ -6,11 +6,11 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
@ -52,7 +52,7 @@ type containerOptions struct {
deviceWriteBps opts.ThrottledeviceOpt
links opts.ListOpts
aliases opts.ListOpts
linkLocalIPs opts.ListOpts
linkLocalIPs opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
deviceReadIOps opts.ThrottledeviceOpt
deviceWriteIOps opts.ThrottledeviceOpt
env opts.ListOpts
@ -64,7 +64,7 @@ type containerOptions struct {
sysctls *opts.MapOpts
publish opts.ListOpts
expose opts.ListOpts
dns opts.ListOpts
dns opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
dnsSearch opts.ListOpts
dnsOptions opts.ListOpts
extraHosts opts.ListOpts
@ -112,8 +112,8 @@ type containerOptions struct {
swappiness int64
netMode opts.NetworkOpt
macAddress string
ipv4Address string
ipv6Address string
ipv4Address 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
ipcMode string
pidsLimit int64
restartPolicy string
@ -239,8 +239,8 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.MarkHidden("dns-opt")
flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains")
flags.Var(&copts.expose, "expose", "Expose a port or a range of ports")
flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)")
flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)")
flags.IPVar(&copts.ipv4Address, "ip", nil, "IPv4 address (e.g., 172.30.100.104)")
flags.IPVar(&copts.ipv6Address, "ip6", nil, "IPv6 address (e.g., 2001:db8::33)")
flags.Var(&copts.links, "link", "Add link to another container")
flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses")
flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)")
@ -426,38 +426,60 @@ 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,
})
}
}
// Add published ports as exposed ports.
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() {
if strings.Contains(e, ":") {
return nil, fmt.Errorf("invalid port format for --expose: %s", e)
}
// support two formats for expose, original format <portnum>/[<proto>]
// or <startport-endport>/[<proto>]
proto, port := nat.SplitProtoPort(e)
pr, err := network.ParsePortRange(e)
if err != nil {
return nil, fmt.Errorf("invalid range format for --expose: %w", err)
}
// parse the start and end port and create a sequence of ports to expose
// if expose a port, the start and end port are the same
start, end, err := nat.ParsePortRange(port)
if err != nil {
return nil, fmt.Errorf("invalid range format for --expose: %s, error: %w", e, err)
}
for i := start; i <= end; i++ {
p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
if err != nil {
return nil, err
}
if _, exists := ports[p]; !exists {
ports[p] = struct{}{}
}
for p := range pr.All() {
exposedPorts[p] = struct{}{}
}
}
@ -626,7 +648,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,
@ -662,7 +684,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(),
@ -805,10 +827,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 != nil {
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 != nil {
return invalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address"))
}
if n.MacAddress != "" && copts.macAddress != "" {
@ -827,18 +849,21 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
n.Links = make([]string, copts.links.Len())
copy(n.Links, copts.links.GetSlice())
}
if copts.ipv4Address != "" {
n.IPv4Address = copts.ipv4Address
if copts.ipv4Address != nil {
if ipv4, ok := netip.AddrFromSlice(copts.ipv4Address.To4()); ok {
n.IPv4Address = ipv4
}
}
if copts.ipv6Address != "" {
n.IPv6Address = copts.ipv6Address
if copts.ipv6Address != nil {
if ipv6, ok := netip.AddrFromSlice(copts.ipv6Address.To16()); ok {
n.IPv6Address = ipv6
}
}
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
}
@ -867,7 +892,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,
@ -1131,3 +1156,18 @@ func validateAttach(val string) (string, error) {
}
return val, errors.New("valid streams are STDIN, STDOUT and STDERR")
}
func toNetipAddrSlice(ips []string) []netip.Addr {
if len(ips) == 0 {
return nil
}
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
}

View File

@ -4,12 +4,14 @@ import (
"errors"
"fmt"
"io"
"net/netip"
"os"
"runtime"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
networktypes "github.com/moby/moby/api/types/network"
"github.com/spf13/pflag"
@ -430,14 +432,14 @@ func TestParseHostnameDomainname(t *testing.T) {
func TestParseWithExpose(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
tests := map[string]string{
":": "invalid port format for --expose: :",
"8080:9090": "invalid port format for --expose: 8080:9090",
"/tcp": "invalid range format for --expose: /tcp, error: empty string specified for ports",
"/udp": "invalid range format for --expose: /udp, error: empty string specified for ports",
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"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`,
":": `invalid range format for --expose: invalid start port ':': invalid syntax`,
"8080:9090": `invalid range format for --expose: invalid start port '8080:9090': invalid syntax`,
"/tcp": `invalid range format for --expose: invalid start port '': value is empty`,
"/udp": `invalid range format for --expose: invalid start port '': value is empty`,
"NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
"NaN-NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
"8080-NaN/tcp": `invalid range format for --expose: invalid end port 'NaN': invalid syntax`,
"1234567890-8080/tcp": `invalid range format for --expose: invalid start port '1234567890': value out of range`,
}
for expose, expectedError := range tests {
t.Run(expose, func(t *testing.T) {
@ -447,12 +449,12 @@ func TestParseWithExpose(t *testing.T) {
}
})
t.Run("valid", func(t *testing.T) {
tests := 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"},
tests := 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, exposedPorts := range tests {
t.Run(expose, func(t *testing.T) {
@ -460,7 +462,7 @@ func TestParseWithExpose(t *testing.T) {
assert.NilError(t, err)
for _, port := range exposedPorts {
_, ok := config.ExposedPorts[port]
assert.Check(t, ok, "missing port %q in exposed ports", port)
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
}
})
}
@ -471,10 +473,10 @@ func TestParseWithExpose(t *testing.T) {
config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
assert.NilError(t, err)
assert.Check(t, is.Len(config.ExposedPorts, 2))
ports := []container.PortRangeProto{"80/tcp", "81/tcp"}
ports := []networktypes.Port{networktypes.MustParsePort("80/tcp"), networktypes.MustParsePort("81/tcp")}
for _, port := range ports {
_, ok := config.ExposedPorts[port]
assert.Check(t, ok, "missing port %q in exposed ports", port)
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
}
})
}
@ -606,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"},
@ -636,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"},
@ -647,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")},
},
},
},
@ -671,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",
@ -753,7 +755,7 @@ func TestParseNetworkConfig(t *testing.T) {
assert.NilError(t, err)
assert.DeepEqual(t, config.MacAddress, tc.expectedCfg.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode)
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected)
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected, cmpopts.EquateComparable(netip.Addr{}))
})
}
}

View File

@ -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)))
}
}
}

View File

@ -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",
}
}

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/client"
)
func TestContainerPrunePromptTermination(t *testing.T) {
@ -16,7 +16,7 @@ func TestContainerPrunePromptTermination(t *testing.T) {
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
containerPruneFunc: func(ctx context.Context, pruneFilters client.Filters) (container.PruneReport, error) {
return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
},
})

View File

@ -10,13 +10,13 @@ import (
"sync"
"time"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -60,7 +60,7 @@ type StatsOptions struct {
// filter options may be added in future (within the constraints described
// above), but may require daemon-side validation as the list of accepted
// filters can differ between daemon- and API versions.
Filters *filters.Args
Filters client.Filters
}
// newStatsCommand creates a new [cobra.Command] for "docker container stats".
@ -101,6 +101,24 @@ var acceptedStatsFilters = map[string]bool{
"label": true,
}
// cloneFilters returns a deep copy of f.
//
// TODO(thaJeztah): add this as a "Clone" method on client.Filters.
func cloneFilters(f client.Filters) client.Filters {
if f == nil {
return nil
}
out := make(client.Filters, len(f))
for term, values := range f {
inner := make(map[string]bool, len(values))
for v, ok := range values {
inner[v] = ok
}
out[term] = inner
}
return out
}
// RunStats displays a live stream of resource usage statistics for one or more containers.
// This shows real-time information on CPU usage, memory usage, and network I/O.
//
@ -123,12 +141,14 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
started := make(chan struct{})
if options.Filters == nil {
f := filters.NewArgs()
options.Filters = &f
options.Filters = make(client.Filters)
}
if err := options.Filters.Validate(acceptedStatsFilters); err != nil {
return err
// FIXME(thaJeztah): any way we can (and should?) validate allowed filters?
for filter := range options.Filters {
if _, ok := acceptedStatsFilters[filter]; !ok {
return errdefs.ErrInvalidArgument.WithMessage("invalid filter '" + filter + "'")
}
}
eh := newEventHandler()
@ -163,7 +183,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
// the original set of filters. Custom filters are used both
// to list containers and to filter events, but the "type" filter
// is not valid for filtering containers.
f := options.Filters.Clone()
f := cloneFilters(options.Filters)
f.Add("type", string(events.ContainerEventType))
eventChan, errChan := apiClient.Events(ctx, client.EventsListOptions{
Filters: f,
@ -199,7 +219,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
// to refresh the list of containers.
cs, err := apiClient.ContainerList(ctx, client.ContainerListOptions{
All: options.All,
Filters: *options.Filters,
Filters: options.Filters,
})
if err != nil {
return err
@ -219,7 +239,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
// only a single code-path is needed, and custom filters can be combined
// with a list of container names/IDs.
if options.Filters != nil && options.Filters.Len() > 0 {
if len(options.Filters) > 0 {
return errors.New("filtering is not supported when specifying a list of containers")
}

View File

@ -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.IsValid() {
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 {

View File

@ -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",

View File

@ -6,7 +6,6 @@ import (
"strings"
"time"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
@ -19,8 +18,8 @@ type fakeClient struct {
imageRemoveFunc func(image string, options client.ImageRemoveOptions) ([]image.DeleteResponse, error)
imagePushFunc func(ref string, options client.ImagePushOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error)
imagePullFunc func(ref string, options client.ImagePullOptions) (io.ReadCloser, error)
imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error)
imagePullFunc func(ref string, options client.ImagePullOptions) (client.ImagePullResponse, error)
imagesPruneFunc func(pruneFilter client.Filters) (image.PruneReport, error)
imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (client.LoadResponse, error)
imageListFunc func(options client.ImageListOptions) ([]image.Summary, error)
imageInspectFunc func(img string) (image.InspectResponse, error)
@ -66,14 +65,14 @@ func (cli *fakeClient) Info(_ context.Context) (system.Info, error) {
return system.Info{}, nil
}
func (cli *fakeClient) ImagePull(_ context.Context, ref string, options client.ImagePullOptions) (io.ReadCloser, error) {
func (cli *fakeClient) ImagePull(_ context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
if cli.imagePullFunc != nil {
return cli.imagePullFunc(ref, options)
}
return io.NopCloser(strings.NewReader("")), nil
return client.ImagePullResponse{}, nil
}
func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter filters.Args) (image.PruneReport, error) {
func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter client.Filters) (image.PruneReport, error) {
if cli.imagesPruneFunc != nil {
return cli.imagesPruneFunc(pruneFilter)
}

View File

@ -11,7 +11,6 @@ import (
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
@ -69,7 +68,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
name: "match-name",
args: []string{"image"},
imageListFunc: func(options client.ImageListOptions) ([]image.Summary, error) {
assert.Check(t, is.Equal("image", options.Filters.Get("reference")[0]))
assert.Check(t, options.Filters["reference"]["image"])
return []image.Summary{}, nil
},
},
@ -77,7 +76,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
name: "filters",
args: []string{"--filter", "name=value"},
imageListFunc: func(options client.ImageListOptions) ([]image.Summary, error) {
assert.Check(t, is.Equal("value", options.Filters.Get("name")[0]))
assert.Check(t, options.Filters["name"]["value"])
return []image.Summary{}, nil
},
},

View File

@ -69,9 +69,8 @@ Are you sure you want to continue?`
)
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := options.filter.Value().Clone()
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
pruneFilters.Add("dangling", strconv.FormatBool(!options.all))
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
warning := danglingWarning
if options.all {

View File

@ -10,10 +10,9 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
@ -22,7 +21,7 @@ func TestNewPruneCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error)
imagesPruneFunc func(pruneFilter client.Filters) (image.PruneReport, error)
}{
{
name: "wrong-args",
@ -33,7 +32,7 @@ func TestNewPruneCommandErrors(t *testing.T) {
name: "prune-error",
args: []string{"--force"},
expectedError: "something went wrong",
imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) {
imagesPruneFunc: func(pruneFilter client.Filters) (image.PruneReport, error) {
return image.PruneReport{}, errors.New("something went wrong")
},
},
@ -55,21 +54,21 @@ func TestNewPruneCommandSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error)
imagesPruneFunc func(pruneFilter client.Filters) (image.PruneReport, error)
}{
{
name: "all",
args: []string{"--all"},
imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) {
assert.Check(t, is.Equal("false", pruneFilter.Get("dangling")[0]))
imagesPruneFunc: func(pruneFilter client.Filters) (image.PruneReport, error) {
assert.Check(t, pruneFilter["dangling"]["false"])
return image.PruneReport{}, nil
},
},
{
name: "force-deleted",
args: []string{"--force"},
imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) {
assert.Check(t, is.Equal("true", pruneFilter.Get("dangling")[0]))
imagesPruneFunc: func(pruneFilter client.Filters) (image.PruneReport, error) {
assert.Check(t, pruneFilter["dangling"]["true"])
return image.PruneReport{
ImagesDeleted: []image.DeleteResponse{{Deleted: "image1"}},
SpaceReclaimed: 1,
@ -79,16 +78,16 @@ func TestNewPruneCommandSuccess(t *testing.T) {
{
name: "label-filter",
args: []string{"--force", "--filter", "label=foobar"},
imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) {
assert.Check(t, is.Equal("foobar", pruneFilter.Get("label")[0]))
imagesPruneFunc: func(pruneFilter client.Filters) (image.PruneReport, error) {
assert.Check(t, pruneFilter["label"]["foobar"])
return image.PruneReport{}, nil
},
},
{
name: "force-untagged",
args: []string{"--force"},
imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) {
assert.Check(t, is.Equal("true", pruneFilter.Get("dangling")[0]))
imagesPruneFunc: func(pruneFilter client.Filters) (image.PruneReport, error) {
assert.Check(t, pruneFilter["dangling"]["true"])
return image.PruneReport{
ImagesDeleted: []image.DeleteResponse{{Untagged: "image1"}},
SpaceReclaimed: 2,
@ -117,7 +116,7 @@ func TestPrunePromptTermination(t *testing.T) {
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) {
imagesPruneFunc: func(pruneFilter client.Filters) (image.PruneReport, error) {
return image.PruneReport{}, errors.New("fakeClient imagesPruneFunc should not be called")
},
})

View File

@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"strings"
"testing"
"github.com/docker/cli/internal/test"
@ -74,9 +73,9 @@ func TestNewPullCommandSuccess(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options client.ImagePullOptions) (io.ReadCloser, error) {
imagePullFunc: func(ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
assert.Check(t, is.Equal(tc.expectedTag, ref), tc.name)
return io.NopCloser(strings.NewReader("")), nil
return client.ImagePullResponse{}, nil
},
})
cmd := newPullCommand(cli)
@ -120,8 +119,8 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("DOCKER_CONTENT_TRUST", "true")
cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options client.ImagePullOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), errors.New("shouldn't try to pull image")
imagePullFunc: func(ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
return client.ImagePullResponse{}, errors.New("shouldn't try to pull image")
},
})
cli.SetNotaryClient(tc.notaryFunc)

View File

@ -3,10 +3,6 @@
"Id": "",
"RepoTags": null,
"RepoDigests": null,
"Parent": "",
"Comment": "",
"DockerVersion": "",
"Author": "",
"Config": null,
"Architecture": "",
"Os": "",
@ -20,10 +16,6 @@
"Id": "",
"RepoTags": null,
"RepoDigests": null,
"Parent": "",
"Comment": "",
"DockerVersion": "",
"Author": "",
"Config": null,
"Architecture": "",
"Os": "",

View File

@ -3,10 +3,6 @@
"Id": "",
"RepoTags": null,
"RepoDigests": null,
"Parent": "",
"Comment": "",
"DockerVersion": "",
"Author": "",
"Config": null,
"Architecture": "",
"Os": "",

View File

@ -17,7 +17,6 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/tui"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/filters"
imagetypes "github.com/moby/moby/api/types/image"
"github.com/moby/moby/client"
"github.com/morikuni/aec"
@ -26,7 +25,7 @@ import (
type treeOptions struct {
all bool
filters filters.Args
filters client.Filters
}
type treeView struct {

View File

@ -3,7 +3,6 @@ package network
import (
"context"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
)
@ -15,7 +14,7 @@ type fakeClient struct {
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
networkRemoveFunc func(ctx context.Context, networkID string) error
networkListFunc func(ctx context.Context, options client.NetworkListOptions) ([]network.Summary, error)
networkPruneFunc func(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error)
networkPruneFunc func(ctx context.Context, pruneFilters client.Filters) (network.PruneReport, error)
networkInspectFunc func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (network.Inspect, []byte, error)
}
@ -61,7 +60,7 @@ func (c *fakeClient) NetworkInspectWithRaw(ctx context.Context, networkID string
return network.Inspect{}, nil, nil
}
func (c *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) {
func (c *fakeClient) NetworksPrune(ctx context.Context, pruneFilter client.Filters) (network.PruneReport, error) {
if c.networkPruneFunc != nil {
return c.networkPruneFunc(ctx, pruneFilter)
}

View File

@ -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,41 @@ func convertDriverOpt(options []string) (map[string]string, error) {
}
return driverOpt, nil
}
func toNetipAddrSlice(ips []net.IP) []netip.Addr {
if len(ips) == 0 {
return nil
}
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 {
a, _ := netip.AddrFromSlice(ip)
return a.Unmap()
}
// toPrefix converts n into a netip.Prefix. If n is not a valid IPv4 or IPV6
// address, ToPrefix returns netip.Prefix{}, false.
//
// TODO(thaJeztah): create internal package similar to https://github.com/moby/moby/blob/0769fe708773892d6ac399ee137e71a777b35de7/daemon/internal/netiputil/netiputil.go#L21-L42
func toPrefix(n net.IPNet) (netip.Prefix, bool) {
if ll := len(n.Mask); ll != net.IPv4len && ll != net.IPv6len {
return netip.Prefix{}, false
}
addr, ok := netip.AddrFromSlice(n.IP)
if !ok {
return netip.Prefix{}, false
}
ones, bits := n.Mask.Size()
if ones == 0 && bits == 0 {
return netip.Prefix{}, false
}
return netip.PrefixFrom(addr.Unmap(), ones), true
}

View File

@ -4,9 +4,11 @@ import (
"context"
"errors"
"io"
"net/netip"
"testing"
"github.com/docker/cli/internal/test"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/network"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -46,9 +48,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"},
@ -60,7 +62,7 @@ func TestNetworkConnectWithFlags(t *testing.T) {
}
cli := test.NewFakeCli(&fakeClient{
networkConnectFunc: func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
assert.Check(t, is.DeepEqual(expectedConfig, config))
assert.Check(t, is.DeepEqual(expectedConfig, config, cmpopts.EquateComparable(netip.Addr{})))
return nil
},
})

View File

@ -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")
@ -162,32 +163,36 @@ 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 := netip.ParsePrefix(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 {
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 != "" {
return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s)
// 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.String(), iData[s].IPRange.String(), s)
}
if ipRange, ok := toPrefix(r); ok {
iData[s].IPRange = ipRange
match = true
}
d := iData[s]
d.IPRange = r
match = true
}
if !match {
return nil, fmt.Errorf("no matching subnet for range %s", r)
return nil, fmt.Errorf("no matching subnet for range %s", r.String())
}
}
@ -195,18 +200,18 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) {
for _, g := range options.gateways {
match := false
for _, s := range options.subnets {
ok, err := subnetMatches(s, g)
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 {
@ -215,17 +220,24 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) {
}
// Validate and add aux-addresses
for key, aa := range options.auxAddresses.GetAll() {
for name, aa := range options.auxAddresses.GetAll() {
if aa == "" {
continue
}
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[name] = auxAddr
match = true
}
if !match {

View File

@ -4,10 +4,12 @@ import (
"context"
"errors"
"io"
"net/netip"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
@ -35,24 +37,15 @@ func TestNetworkCreateErrors(t *testing.T) {
args: []string{"toto"},
flags: map[string]string{
"ip-range": "255.255.0.0/24",
"gateway": "255.0.255.0/24",
"gateway": "255.0.255.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.255.0/24")
"subnet": "10.1.2.0.30.50",
},
expectedError: "invalid CIDR address: 10.1.2.0.30.50",
expectedError: `netip.ParsePrefix("10.1.2.0.30.50"): no '/'`,
},
{
args: []string{"toto"},
flags: map[string]string{
"ip-range": "255.255.0.0.30/24",
"gateway": "255.0.255.0/24",
"subnet": "255.0.0.0/24",
},
expectedError: "invalid CIDR address: 255.255.0.0.30/24",
},
{
args: []string{"toto"},
flags: map[string]string{
"gateway": "255.0.0.0/24",
"gateway": "255.0.0.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24")
},
expectedError: "every ip-range or gateway must have a corresponding subnet",
},
@ -67,7 +60,7 @@ func TestNetworkCreateErrors(t *testing.T) {
args: []string{"toto"},
flags: map[string]string{
"ip-range": "255.0.0.0/24",
"gateway": "255.0.0.0/24",
"gateway": "255.0.0.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24")
},
expectedError: "every ip-range or gateway must have a corresponding subnet",
},
@ -75,7 +68,7 @@ func TestNetworkCreateErrors(t *testing.T) {
args: []string{"toto"},
flags: map[string]string{
"ip-range": "255.255.0.0/24",
"gateway": "255.0.255.0/24",
"gateway": "255.0.255.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24")
"subnet": "10.1.2.0/23,10.1.3.248/30",
},
expectedError: "multiple overlapping subnet configuration is not supported",
@ -83,17 +76,17 @@ func TestNetworkCreateErrors(t *testing.T) {
{
args: []string{"toto"},
flags: map[string]string{
"ip-range": "192.168.1.0/24,192.168.1.200/24",
"ip-range": "192.168.1.0/25,192.168.1.128/25",
"gateway": "192.168.1.1,192.168.1.4",
"subnet": "192.168.2.0/24,192.168.1.250/24",
},
expectedError: "cannot configure multiple ranges (192.168.1.200/24, 192.168.1.0/24) on the same subnet (192.168.1.250/24)",
expectedError: "cannot configure multiple ranges (192.168.1.128/25, 192.168.1.0/25) on the same subnet (192.168.1.250/24)",
},
{
args: []string{"toto"},
flags: map[string]string{
"ip-range": "255.255.200.0/24,255.255.120.0/24",
"gateway": "255.0.255.0/24",
"gateway": "255.0.255.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24")
"subnet": "255.255.255.0/24,255.255.0.255/24",
},
expectedError: "no matching subnet for range 255.255.200.0/24",
@ -119,21 +112,12 @@ func TestNetworkCreateErrors(t *testing.T) {
{
args: []string{"toto"},
flags: map[string]string{
"gateway": "255.255.0.0/24",
"gateway": "255.255.0.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.255.0.0/24")
"subnet": "255.255.0.0/24",
"aux-address": "255.255.0.30/24",
"aux-address": "router=255.255.1.30", // outside 255.255.0.0/24 // FIXME(thaJeztah): this used to accept a CIDR ("255.255.0.30/24")
},
expectedError: "no matching subnet for aux-address",
},
{
args: []string{"toto"},
flags: map[string]string{
"ip-range": "192.168.83.1-192.168.83.254",
"gateway": "192.168.80.1",
"subnet": "192.168.80.0/20",
},
expectedError: "invalid CIDR address: 192.168.83.1-192.168.83.254",
},
}
for _, tc := range testCases {
@ -175,16 +159,16 @@ 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"), // FIXME(thaJeztah): this used to accept a CIDR ("192.168.4.1/24")
AuxAddress: map[string]netip.Addr{},
},
}
cli := test.NewFakeCli(&fakeClient{
networkCreateFunc: func(ctx context.Context, name string, options client.NetworkCreateOptions) (network.CreateResponse, error) {
assert.Check(t, is.Equal(expectedDriver, options.Driver), "not expected driver error")
assert.Check(t, is.DeepEqual(expectedOpts, options.IPAM.Config), "not expected driver error")
assert.Check(t, is.DeepEqual(expectedOpts, options.IPAM.Config, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})), "not expected driver error")
return network.CreateResponse{
ID: name,
}, nil
@ -196,7 +180,7 @@ func TestNetworkCreateWithFlags(t *testing.T) {
cmd.SetArgs(args)
assert.Check(t, cmd.Flags().Set("driver", "foo"))
assert.Check(t, cmd.Flags().Set("ip-range", "192.168.4.0/24"))
assert.Check(t, cmd.Flags().Set("gateway", "192.168.4.1/24"))
assert.Check(t, cmd.Flags().Set("gateway", "192.168.4.1")) // FIXME(thaJeztah): this used to accept a CIDR ("192.168.4.1/24")
assert.Check(t, cmd.Flags().Set("subnet", "192.168.4.0/24"))
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("banana", strings.TrimSpace(cli.OutBuffer().String())))

View File

@ -8,8 +8,6 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/google/go-cmp/cmp"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
@ -57,9 +55,9 @@ func TestNetworkList(t *testing.T) {
golden: "network-list.golden",
networkListFunc: func(ctx context.Context, options client.NetworkListOptions) ([]network.Summary, error) {
expectedOpts := client.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("image.name", "ubuntu")),
Filters: make(client.Filters).Add("image.name", "ubuntu"),
}
assert.Check(t, is.DeepEqual(expectedOpts, options, cmp.AllowUnexported(filters.Args{})))
assert.Check(t, is.DeepEqual(expectedOpts, options))
return []network.Summary{*builders.NetworkResource(builders.NetworkResourceID("123454321"),
builders.NetworkResourceName("network_1"),

View File

@ -7,8 +7,8 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
)
func TestNetworkPrunePromptTermination(t *testing.T) {
@ -16,7 +16,7 @@ func TestNetworkPrunePromptTermination(t *testing.T) {
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error) {
networkPruneFunc: func(ctx context.Context, pruneFilters client.Filters) (network.PruneReport, error) {
return network.PruneReport{}, errors.New("fakeClient networkPruneFunc should not be called")
},
})

View File

@ -4,7 +4,6 @@ import (
"context"
"io"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/plugin"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
@ -17,7 +16,7 @@ type fakeClient struct {
pluginEnableFunc func(name string, options client.PluginEnableOptions) error
pluginRemoveFunc func(name string, options client.PluginRemoveOptions) error
pluginInstallFunc func(name string, options client.PluginInstallOptions) (io.ReadCloser, error)
pluginListFunc func(filter filters.Args) (plugin.ListResponse, error)
pluginListFunc func(filter client.Filters) (plugin.ListResponse, error)
pluginInspectFunc func(name string) (*plugin.Plugin, []byte, error)
pluginUpgradeFunc func(name string, options client.PluginInstallOptions) (io.ReadCloser, error)
}
@ -57,7 +56,7 @@ func (c *fakeClient) PluginInstall(_ context.Context, name string, installOption
return nil, nil
}
func (c *fakeClient) PluginList(_ context.Context, filter filters.Args) (plugin.ListResponse, error) {
func (c *fakeClient) PluginList(_ context.Context, filter client.Filters) (plugin.ListResponse, error) {
if c.pluginListFunc != nil {
return c.pluginListFunc(filter)
}

View File

@ -2,7 +2,7 @@ package plugin
import (
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -22,7 +22,7 @@ const (
// - "disabled": all disabled plugins
func completeNames(dockerCLI completion.APIClientProvider, state pluginState) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
f := filters.NewArgs()
f := make(client.Filters)
switch state {
case stateEnabled:
f.Add("enabled", "true")

View File

@ -6,11 +6,10 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/plugin"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
@ -20,7 +19,7 @@ func TestListErrors(t *testing.T) {
args []string
flags map[string]string
expectedError string
listFunc func(filter filters.Args) (plugin.ListResponse, error)
listFunc func(filter client.Filters) (plugin.ListResponse, error)
}{
{
description: "too many arguments",
@ -31,7 +30,7 @@ func TestListErrors(t *testing.T) {
description: "error listing plugins",
args: []string{},
expectedError: "error listing plugins",
listFunc: func(filter filters.Args) (plugin.ListResponse, error) {
listFunc: func(filter client.Filters) (plugin.ListResponse, error) {
return plugin.ListResponse{}, errors.New("error listing plugins")
},
},
@ -61,7 +60,7 @@ func TestListErrors(t *testing.T) {
}
func TestList(t *testing.T) {
singlePluginListFunc := func(_ filters.Args) (plugin.ListResponse, error) {
singlePluginListFunc := func(_ client.Filters) (plugin.ListResponse, error) {
return plugin.ListResponse{
{
ID: "id-foo",
@ -79,7 +78,7 @@ func TestList(t *testing.T) {
args []string
flags map[string]string
golden string
listFunc func(filter filters.Args) (plugin.ListResponse, error)
listFunc func(filter client.Filters) (plugin.ListResponse, error)
}{
{
description: "list with no additional flags",
@ -94,8 +93,8 @@ func TestList(t *testing.T) {
"filter": "foo=bar",
},
golden: "plugin-list-without-format.golden",
listFunc: func(filter filters.Args) (plugin.ListResponse, error) {
assert.Check(t, is.Equal("bar", filter.Get("foo")[0]))
listFunc: func(filter client.Filters) (plugin.ListResponse, error) {
assert.Check(t, filter["foo"]["bar"])
return singlePluginListFunc(filter)
},
},
@ -116,7 +115,7 @@ func TestList(t *testing.T) {
"format": "{{ .ID }}",
},
golden: "plugin-list-with-no-trunc-option.golden",
listFunc: func(_ filters.Args) (plugin.ListResponse, error) {
listFunc: func(_ client.Filters) (plugin.ListResponse, error) {
return plugin.ListResponse{
{
ID: "xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd",
@ -145,7 +144,7 @@ func TestList(t *testing.T) {
"format": "{{ .Name }}",
},
golden: "plugin-list-sort.golden",
listFunc: func(_ filters.Args) (plugin.ListResponse, error) {
listFunc: func(_ client.Filters) (plugin.ListResponse, error) {
return plugin.ListResponse{
{
ID: "id-1",

View File

@ -57,7 +57,7 @@ func newSearchCommand(dockerCLI command.Cli) *cobra.Command {
}
func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions) error {
if options.filter.Value().Contains("is-automated") {
if _, ok := options.filter.Value()["is-automated"]; ok {
_, _ = fmt.Fprintln(dockerCli.Err(), `WARNING: the "is-automated" filter is deprecated, and searching for "is-automated=true" will not yield any results in future.`)
}
encodedAuth, err := getAuth(dockerCli, options.term)

View File

@ -13,7 +13,6 @@ import (
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
@ -135,8 +134,8 @@ func TestSecretListWithFormat(t *testing.T) {
func TestSecretListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
secretListFunc: func(_ context.Context, options client.SecretListOptions) ([]swarm.Secret, error) {
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]), "foo")
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
assert.Check(t, options.Filters["name"]["foo"])
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"])
return []swarm.Secret{
*builders.Secret(builders.SecretID("ID-foo"),
builders.SecretName("foo"),

View File

@ -68,13 +68,9 @@ func TestSetConfigsWithCredSpecAndConfigs(t *testing.T) {
// set up a function to use as the list function
var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts client.ConfigListOptions) ([]swarm.Config, error) {
f := opts.Filters
// we're expecting the filter to have names "foo" and "bar"
names := f.Get("name")
assert.Equal(t, len(names), 2)
assert.Assert(t, is.Contains(names, "foo"))
assert.Assert(t, is.Contains(names, "bar"))
expected := make(client.Filters).Add("name", "foo", "bar")
assert.Assert(t, is.DeepEqual(opts.Filters, expected))
return []swarm.Config{
{
@ -148,11 +144,8 @@ func TestSetConfigsOnlyCredSpec(t *testing.T) {
// set up a function to use as the list function
var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts client.ConfigListOptions) ([]swarm.Config, error) {
f := opts.Filters
names := f.Get("name")
assert.Equal(t, len(names), 1)
assert.Assert(t, is.Contains(names, "foo"))
expected := make(client.Filters).Add("name", "foo")
assert.Assert(t, is.DeepEqual(opts.Filters, expected))
return []swarm.Config{
{
@ -199,11 +192,8 @@ func TestSetConfigsOnlyConfigs(t *testing.T) {
}
var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts client.ConfigListOptions) ([]swarm.Config, error) {
f := opts.Filters
names := f.Get("name")
assert.Equal(t, len(names), 1)
assert.Assert(t, is.Contains(names, "bar"))
expected := make(client.Filters).Add("name", "bar")
assert.Assert(t, is.DeepEqual(opts.Filters, expected))
return []swarm.Config{
{

View File

@ -735,7 +735,7 @@ type portRange struct {
pEnd uint32
tStart uint32
tEnd uint32
protocol swarm.PortConfigProtocol
protocol network.IPProtocol
}
func (pr portRange) String() string {

View File

@ -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.MustParsePrefix("10.255.0.4/16"), // FIXME(thaJeztah): this was testing with "10.255.0.4/16"
},
},
},

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
@ -109,7 +108,7 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er
// that don't have ServiceStatus set, and perform a lookup for those.
func AppendServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) ([]swarm.Service, error) {
status := map[string]*swarm.ServiceStatus{}
taskFilter := filters.NewArgs()
taskFilter := make(client.Filters)
for i, s := range services {
// there is no need in this switch to check for job modes. jobs are not
// supported until after ServiceStatus was introduced.

View File

@ -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,18 @@ const (
flagUlimitRemove = "ulimit-rm"
flagOomScoreAdj = "oom-score-adj"
)
func toNetipAddrSlice(ips []string) []netip.Addr {
if len(ips) == 0 {
return nil
}
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
}

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
)
@ -27,7 +26,7 @@ func ParseSecrets(ctx context.Context, apiClient client.SecretAPIClient, request
secretRefs[secret.File.Name] = secretRef
}
args := filters.NewArgs()
args := make(client.Filters)
for _, s := range secretRefs {
args.Add("name", s.SecretName)
}
@ -104,7 +103,7 @@ func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, request
configRefs[config.File.Name] = configRef
}
args := filters.NewArgs()
args := make(client.Filters)
for _, s := range configRefs {
args.Add("name", s.ConfigName)
}

View File

@ -14,7 +14,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/pkg/progress"
"github.com/moby/moby/api/pkg/streamformatter"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
)
@ -142,10 +141,9 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID
return nil
}
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{Filters: filters.NewArgs(
filters.KeyValuePair{Key: "service", Value: service.ID},
filters.KeyValuePair{Key: "_up-to-date", Value: "true"},
)})
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", service.ID).Add("_up-to-date", "true"),
})
if err != nil {
return err
}

View File

@ -11,7 +11,6 @@ import (
"github.com/docker/cli/cli/command/node"
"github.com/docker/cli/cli/command/task"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -81,11 +80,11 @@ func runPS(ctx context.Context, dockerCli command.Cli, options psOptions) error
return nil
}
func createFilter(ctx context.Context, apiClient client.APIClient, options psOptions) (filters.Args, []string, error) {
func createFilter(ctx context.Context, apiClient client.APIClient, options psOptions) (client.Filters, []string, error) {
filter := options.filter.Value()
serviceIDFilter := filters.NewArgs()
serviceNameFilter := filters.NewArgs()
serviceIDFilter := make(client.Filters)
serviceNameFilter := make(client.Filters)
for _, service := range options.services {
serviceIDFilter.Add("id", service)
serviceNameFilter.Add("name", service)
@ -139,15 +138,15 @@ loop:
return filter, notfound, err
}
func updateNodeFilter(ctx context.Context, apiClient client.APIClient, filter filters.Args) error {
if filter.Contains("node") {
nodeFilters := filter.Get("node")
for _, nodeFilter := range nodeFilters {
func updateNodeFilter(ctx context.Context, apiClient client.APIClient, filter client.Filters) error {
if nodeFilters, ok := filter["node"]; ok {
for nodeFilter := range nodeFilters {
nodeReference, err := node.Reference(ctx, apiClient, nodeFilter)
if err != nil {
return err
}
filter.Del("node", nodeFilter)
// TODO(thaJeztah): add utility to remove?
delete(filter["node"], nodeFilter)
filter.Add("node", nodeReference)
}
}

View File

@ -6,8 +6,6 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/opts"
"github.com/google/go-cmp/cmp"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
@ -38,13 +36,8 @@ func TestCreateFilter(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(notfound, []string{"no such service: notfound"}))
expected := filters.NewArgs(
filters.Arg("service", "idmatch"),
filters.Arg("service", "idprefixmatch"),
filters.Arg("service", "cccccccc"),
filters.Arg("node", "somenode"),
)
assert.DeepEqual(t, expected, actual, cmpFilters)
expected := make(client.Filters).Add("service", "idmatch").Add("service", "idprefixmatch").Add("service", "cccccccc").Add("node", "somenode")
assert.DeepEqual(t, expected, actual)
}
func TestCreateFilterWithAmbiguousIDPrefixError(t *testing.T) {
@ -114,11 +107,7 @@ func TestRunPSQuiet(t *testing.T) {
func TestUpdateNodeFilter(t *testing.T) {
selfNodeID := "foofoo"
filter := filters.NewArgs(
filters.Arg("node", "one"),
filters.Arg("node", "two"),
filters.Arg("node", "self"),
)
filter := make(client.Filters).Add("node", "one", "two", "self")
apiClient := &fakeClient{
infoFunc: func(_ context.Context) (system.Info, error) {
@ -129,12 +118,6 @@ func TestUpdateNodeFilter(t *testing.T) {
err := updateNodeFilter(context.Background(), apiClient, filter)
assert.NilError(t, err)
expected := filters.NewArgs(
filters.Arg("node", "one"),
filters.Arg("node", "two"),
filters.Arg("node", selfNodeID),
)
assert.DeepEqual(t, expected, filter, cmpFilters)
expected := make(client.Filters).Add("node", "one", "two", selfNodeID)
assert.DeepEqual(t, expected, filter)
}
var cmpFilters = cmp.AllowUnexported(filters.Args{})

View File

@ -1,9 +1,14 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23
package service
import (
"context"
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strings"
"time"
@ -15,6 +20,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,9 +1005,9 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error {
return nil
}
func removeDuplicates(entries []string) []string {
hit := map[string]bool{}
newEntries := []string{}
func removeDuplicates[T comparable](entries []T) []T {
hit := map[T]bool{}
newEntries := []T{}
for _, v := range entries {
if !hit[v] {
newEntries = append(newEntries, v)
@ -1017,24 +1023,34 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error {
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)
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 +1065,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 +1136,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 {

View File

@ -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"},
}
@ -221,9 +222,9 @@ func TestUpdateDNSConfig(t *testing.T) {
updateDNSConfig(flags, &config)
assert.Assert(t, is.Len(config.Nameservers, 3))
assert.Check(t, is.Equal("1.1.1.1", config.Nameservers[0]))
assert.Check(t, is.Equal("2001:db8:abc8::1", config.Nameservers[1]))
assert.Check(t, is.Equal("5.5.5.5", config.Nameservers[2]))
assert.Check(t, is.Equal(netip.MustParseAddr("1.1.1.1"), config.Nameservers[0]))
assert.Check(t, is.Equal(netip.MustParseAddr("5.5.5.5"), config.Nameservers[1]))
assert.Check(t, is.Equal(netip.MustParseAddr("2001:db8:abc8::1"), config.Nameservers[2]))
assert.Assert(t, is.Len(config.Search, 2))
assert.Check(t, is.Equal("example.com", config.Search[0]))
@ -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,
},
}
@ -1219,12 +1220,12 @@ func TestUpdateGetUpdatedConfigs(t *testing.T) {
// fakeConfigAPIClientList is actually defined in create_test.go,
// but we'll use it here as well
var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts client.ConfigListOptions) ([]swarm.Config, error) {
names := opts.Filters.Get("name")
names := opts.Filters["name"]
assert.Equal(t, len(names), len(tc.lookupConfigs))
configs := []swarm.Config{}
for _, lookup := range tc.lookupConfigs {
assert.Assert(t, is.Contains(names, lookup))
assert.Assert(t, names[lookup])
cfg, ok := cannedConfigs[lookup]
assert.Assert(t, ok)
configs = append(configs, *cfg)

View File

@ -6,7 +6,6 @@ import (
"github.com/docker/cli/cli/compose/convert"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
@ -226,8 +225,13 @@ func configFromName(name string) swarm.Config {
}
}
func namespaceFromFilters(fltrs filters.Args) string {
label := fltrs.Get("label")[0]
func namespaceFromFilters(fltrs client.Filters) string {
// FIXME(thaJeztah): more elegant way for this? Should we have a utility for this?
var label string
for fltr := range fltrs["label"] {
label = fltr
break
}
return strings.TrimPrefix(label, convert.LabelNamespace+"=")
}

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
@ -38,22 +37,18 @@ func quotesOrWhitespace(r rune) bool {
return unicode.IsSpace(r) || r == '"' || r == '\''
}
func getStackFilter(namespace string) filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace+"="+namespace)
return filter
func getStackFilter(namespace string) client.Filters {
return make(client.Filters).Add("label", convert.LabelNamespace+"="+namespace)
}
func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) client.Filters {
filter := opt.Value()
filter.Add("label", convert.LabelNamespace+"="+namespace)
return filter
}
func getAllStacksFilter() filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace)
return filter
func getAllStacksFilter() client.Filters {
return make(client.Filters).Add("label", convert.LabelNamespace)
}
func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) {

View File

@ -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
},

View File

@ -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): change opts.defaultAddrPools a []netip.Prefix; see https://github.com/docker/cli/pull/6545#discussion_r2420361609
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(),

View File

@ -6,7 +6,6 @@ import (
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
@ -20,12 +19,12 @@ type fakeClient struct {
version string
containerListFunc func(context.Context, client.ContainerListOptions) ([]container.Summary, error)
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
containerPruneFunc func(ctx context.Context, pruneFilters client.Filters) (container.PruneReport, error)
eventsFn func(context.Context, client.EventsListOptions) (<-chan events.Message, <-chan error)
imageListFunc func(ctx context.Context, options client.ImageListOptions) ([]image.Summary, error)
infoFunc func(ctx context.Context) (system.Info, error)
networkListFunc func(ctx context.Context, options client.NetworkListOptions) ([]network.Summary, error)
networkPruneFunc func(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error)
networkPruneFunc func(ctx context.Context, pruneFilter client.Filters) (network.PruneReport, error)
nodeListFunc func(ctx context.Context, options client.NodeListOptions) ([]swarm.Node, error)
serverVersion func(ctx context.Context) (types.Version, error)
volumeListFunc func(ctx context.Context, options client.VolumeListOptions) (volume.ListResponse, error)
@ -42,7 +41,7 @@ func (cli *fakeClient) ContainerList(ctx context.Context, options client.Contain
return []container.Summary{}, nil
}
func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters client.Filters) (container.PruneReport, error) {
if cli.containerPruneFunc != nil {
return cli.containerPruneFunc(ctx, pruneFilters)
}
@ -74,7 +73,7 @@ func (cli *fakeClient) NetworkList(ctx context.Context, options client.NetworkLi
return []network.Summary{}, nil
}
func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) {
func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter client.Filters) (network.PruneReport, error) {
if cli.networkPruneFunc != nil {
return cli.networkPruneFunc(ctx, pruneFilter)
}

View File

@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/idresolver"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
@ -240,7 +239,7 @@ func nodeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []str
// pluginNames contacts the API to get a list of plugin names.
// In case of an error, an empty list is returned.
func pluginNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().PluginList(cmd.Context(), filters.Args{})
list, err := dockerCLI.Client().PluginList(cmd.Context(), nil)
if err != nil {
return []string{}
}

View File

@ -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)

View File

@ -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: &registrytypes.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,
},
},

View File

@ -161,11 +161,11 @@ func dryRun(ctx context.Context, dockerCli command.Cli, options pruneOptions) (s
var filters []string
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
if pruneFilters.Len() > 0 {
if len(pruneFilters) > 0 {
// TODO remove fixed list of filters, and print all filters instead,
// because the list of filters that is supported by the engine may evolve over time.
for _, name := range []string{"label", "label!", "until"} {
for _, v := range pruneFilters.Get(name) {
for v := range pruneFilters[name] {
filters = append(filters, name+"="+v)
}
}

View File

@ -9,8 +9,8 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -72,10 +72,10 @@ func TestSystemPrunePromptTermination(t *testing.T) {
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
containerPruneFunc: func(ctx context.Context, pruneFilters client.Filters) (container.PruneReport, error) {
return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
},
networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error) {
networkPruneFunc: func(ctx context.Context, pruneFilters client.Filters) (network.PruneReport, error) {
return network.PruneReport{}, errors.New("fakeClient networkPruneFunc should not be called")
},
})

View File

@ -11,17 +11,20 @@ import (
"strings"
"github.com/docker/cli/cli/config"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/client"
)
// PruneFilters merges prune filters specified in config.json with those specified
// as command-line flags.
// as command-line flags. It returns a deep copy of filters to prevent mutating
// the original.
//
// CLI label filters have precedence over those specified in config.json. If a
// label filter specified as flag conflicts with a label defined in config.json
// (i.e., "label=some-value" conflicts with "label!=some-value", and vice versa),
// then the filter defined in config.json is omitted.
func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.Args {
func PruneFilters(dockerCLI config.Provider, filters client.Filters) client.Filters {
pruneFilters := cloneFilters(filters)
cfg := dockerCLI.ConfigFile()
if cfg == nil {
return pruneFilters
@ -37,13 +40,13 @@ func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.
switch k {
case "label":
// "label != some-value" conflicts with "label = some-value"
if pruneFilters.ExactMatch("label!", v) {
if pruneFilters["label!"][v] {
continue
}
pruneFilters.Add(k, v)
case "label!":
// "label != some-value" conflicts with "label = some-value"
if pruneFilters.ExactMatch("label", v) {
if pruneFilters["label"][v] {
continue
}
pruneFilters.Add(k, v)
@ -55,6 +58,21 @@ func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.
return pruneFilters
}
// cloneFilters returns a deep copy of f, creating a new filter if f is nil.
//
// TODO(thaJeztah): add this as a "Clone" method on client.Filters.
func cloneFilters(f client.Filters) client.Filters {
out := make(client.Filters, len(f))
for term, values := range f {
inner := make(map[string]bool, len(values))
for v, ok := range values {
inner[v] = ok
}
out[term] = inner
}
return out
}
// ValidateOutputPath validates the output paths of the "docker cp" command.
func ValidateOutputPath(path string) error {
dir := filepath.Dir(filepath.Clean(path))

View File

@ -3,7 +3,6 @@ package volume
import (
"context"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
)
@ -12,9 +11,9 @@ type fakeClient struct {
client.Client
volumeCreateFunc func(volume.CreateOptions) (volume.Volume, error)
volumeInspectFunc func(volumeID string) (volume.Volume, error)
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
volumeListFunc func(filter client.Filters) (volume.ListResponse, error)
volumeRemoveFunc func(volumeID string, force bool) error
volumePruneFunc func(filter filters.Args) (volume.PruneReport, error)
volumePruneFunc func(filter client.Filters) (volume.PruneReport, error)
}
func (c *fakeClient) VolumeCreate(_ context.Context, options volume.CreateOptions) (volume.Volume, error) {
@ -38,7 +37,7 @@ func (c *fakeClient) VolumeList(_ context.Context, options client.VolumeListOpti
return volume.ListResponse{}, nil
}
func (c *fakeClient) VolumesPrune(_ context.Context, filter filters.Args) (volume.PruneReport, error) {
func (c *fakeClient) VolumesPrune(_ context.Context, filter client.Filters) (volume.PruneReport, error) {
if c.volumePruneFunc != nil {
return c.volumePruneFunc(filter)
}

View File

@ -8,8 +8,8 @@ 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/filters"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
)
@ -18,7 +18,7 @@ func TestVolumeListErrors(t *testing.T) {
testCases := []struct {
args []string
flags map[string]string
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
volumeListFunc func(filter client.Filters) (volume.ListResponse, error)
expectedError string
}{
{
@ -26,7 +26,7 @@ func TestVolumeListErrors(t *testing.T) {
expectedError: "accepts no argument",
},
{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
volumeListFunc: func(filter client.Filters) (volume.ListResponse, error) {
return volume.ListResponse{}, errors.New("error listing volumes")
},
expectedError: "error listing volumes",
@ -50,7 +50,7 @@ func TestVolumeListErrors(t *testing.T) {
func TestVolumeListWithoutFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
volumeListFunc: func(filter client.Filters) (volume.ListResponse, error) {
return volume.ListResponse{
Volumes: []*volume.Volume{
builders.Volume(),
@ -69,7 +69,7 @@ func TestVolumeListWithoutFormat(t *testing.T) {
func TestVolumeListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
volumeListFunc: func(filter client.Filters) (volume.ListResponse, error) {
return volume.ListResponse{
Volumes: []*volume.Volume{
builders.Volume(),
@ -91,7 +91,7 @@ func TestVolumeListWithConfigFormat(t *testing.T) {
func TestVolumeListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
volumeListFunc: func(filter client.Filters) (volume.ListResponse, error) {
return volume.ListResponse{
Volumes: []*volume.Volume{
builders.Volume(),
@ -111,7 +111,7 @@ func TestVolumeListWithFormat(t *testing.T) {
func TestVolumeListSortOrder(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
volumeListFunc: func(filter client.Filters) (volume.ListResponse, error) {
return volume.ListResponse{
Volumes: []*volume.Volume{
builders.Volume(builders.VolumeName("volume-2-foo")),
@ -129,7 +129,7 @@ func TestVolumeListSortOrder(t *testing.T) {
func TestClusterVolumeList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
volumeListFunc: func(filter client.Filters) (volume.ListResponse, error) {
return volume.ListResponse{
Volumes: []*volume.Volume{
{

View File

@ -74,7 +74,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
warning := unusedVolumesWarning
if versions.GreaterThanOrEqualTo(dockerCli.CurrentVersion(), "1.42") {
if options.all {
if pruneFilters.Contains("all") {
if _, ok := pruneFilters["all"]; ok {
return 0, "", invalidParamErr{errors.New("conflicting options: cannot specify both --all and --filter all=1")}
}
pruneFilters.Add("all", "true")
@ -124,7 +124,7 @@ func pruneFn(ctx context.Context, dockerCli command.Cli, options pruner.PruneOpt
// TODO version this once "until" filter is supported for volumes
// Ideally, this check wasn't done on the CLI because the list of
// filters that is supported by the daemon may evolve over time.
if options.Filter.Value().Contains("until") {
if _, ok := options.Filter.Value()["until"]; ok {
return 0, "", errors.New(`ERROR: The "until" filter is not supported with "--volumes"`)
}
if !options.Confirmed {

View File

@ -11,8 +11,8 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
@ -24,7 +24,7 @@ func TestVolumePruneErrors(t *testing.T) {
name string
args []string
flags map[string]string
volumePruneFunc func(args filters.Args) (volume.PruneReport, error)
volumePruneFunc func(args client.Filters) (volume.PruneReport, error)
expectedError string
}{
{
@ -37,7 +37,7 @@ func TestVolumePruneErrors(t *testing.T) {
flags: map[string]string{
"force": "true",
},
volumePruneFunc: func(args filters.Args) (volume.PruneReport, error) {
volumePruneFunc: func(args client.Filters) (volume.PruneReport, error) {
return volume.PruneReport{}, errors.New("error pruning volumes")
},
expectedError: "error pruning volumes",
@ -74,21 +74,21 @@ func TestVolumePruneSuccess(t *testing.T) {
name string
args []string
input string
volumePruneFunc func(args filters.Args) (volume.PruneReport, error)
volumePruneFunc func(args client.Filters) (volume.PruneReport, error)
}{
{
name: "all",
args: []string{"--all"},
input: "y",
volumePruneFunc: func(pruneFilter filters.Args) (volume.PruneReport, error) {
assert.Check(t, is.DeepEqual([]string{"true"}, pruneFilter.Get("all")))
volumePruneFunc: func(pruneFilter client.Filters) (volume.PruneReport, error) {
assert.Check(t, is.DeepEqual(pruneFilter["all"], map[string]bool{"true": true}))
return volume.PruneReport{}, nil
},
},
{
name: "all-forced",
args: []string{"--all", "--force"},
volumePruneFunc: func(pruneFilter filters.Args) (volume.PruneReport, error) {
volumePruneFunc: func(pruneFilter client.Filters) (volume.PruneReport, error) {
return volume.PruneReport{}, nil
},
},
@ -96,8 +96,8 @@ func TestVolumePruneSuccess(t *testing.T) {
name: "label-filter",
args: []string{"--filter", "label=foobar"},
input: "y",
volumePruneFunc: func(pruneFilter filters.Args) (volume.PruneReport, error) {
assert.Check(t, is.DeepEqual([]string{"foobar"}, pruneFilter.Get("label")))
volumePruneFunc: func(pruneFilter client.Filters) (volume.PruneReport, error) {
assert.Check(t, is.DeepEqual(pruneFilter["label"], map[string]bool{"foobar": true}))
return volume.PruneReport{}, nil
},
},
@ -121,7 +121,7 @@ func TestVolumePruneSuccess(t *testing.T) {
func TestVolumePruneForce(t *testing.T) {
testCases := []struct {
name string
volumePruneFunc func(args filters.Args) (volume.PruneReport, error)
volumePruneFunc func(args client.Filters) (volume.PruneReport, error)
}{
{
name: "empty",
@ -180,7 +180,7 @@ func TestVolumePrunePromptNo(t *testing.T) {
}
}
func simplePruneFunc(filters.Args) (volume.PruneReport, error) {
func simplePruneFunc(client.Filters) (volume.PruneReport, error) {
return volume.PruneReport{
VolumesDeleted: []string{
"foo", "bar", "baz",
@ -194,7 +194,7 @@ func TestVolumePrunePromptTerminate(t *testing.T) {
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
volumePruneFunc: func(filter filters.Args) (volume.PruneReport, error) {
volumePruneFunc: func(filter client.Filters) (volume.PruneReport, error) {
return volume.PruneReport{}, errors.New("fakeClient volumePruneFunc should not be called")
},
})

View File

@ -1,6 +1,7 @@
package convert
import (
"net/netip"
"os"
"strings"
@ -84,8 +85,9 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
Driver: nw.Ipam.Driver,
}
for _, ipamConfig := range nw.Ipam.Config {
sn, _ := netip.ParsePrefix(ipamConfig.Subnet)
createOpts.IPAM.Config = append(createOpts.IPAM.Config, network.IPAMConfig{
Subnet: ipamConfig.Subnet,
Subnet: sn,
})
}
}

View File

@ -1,9 +1,11 @@
package convert
import (
"net/netip"
"testing"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
@ -57,7 +59,7 @@ func TestNetworks(t *testing.T) {
Driver: "driver",
Config: []*composetypes.IPAMPool{
{
Subnet: "10.0.0.0",
Subnet: "10.0.0.0/32",
},
},
},
@ -89,7 +91,7 @@ func TestNetworks(t *testing.T) {
Driver: "driver",
Config: []network.IPAMConfig{
{
Subnet: "10.0.0.0",
Subnet: netip.MustParsePrefix("10.0.0.0/32"),
},
},
},
@ -114,7 +116,7 @@ func TestNetworks(t *testing.T) {
}
networks, externals := Networks(namespace, source, serviceNetworks)
assert.DeepEqual(t, expected, networks)
assert.DeepEqual(t, expected, networks, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{}))
assert.DeepEqual(t, []string{"special"}, externals)
}

View File

@ -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,30 @@ 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 {
if len(ips) == 0 {
return nil
}
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

View File

@ -3,12 +3,14 @@ package convert
import (
"context"
"errors"
"net/netip"
"os"
"strings"
"testing"
"time"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
@ -307,17 +309,17 @@ 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))
}, dnsConfig, cmpopts.EquateComparable(netip.Addr{})))
}
func TestConvertDNSConfigNameservers(t *testing.T) {
dnsConfig := convertDNSConfig(nameservers, nil)
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
Nameservers: nameservers,
Nameservers: toNetipAddrSlice(nameservers),
Search: nil,
}, dnsConfig))
}, dnsConfig, cmpopts.EquateComparable(netip.Addr{})))
}
func TestConvertDNSConfigSearch(t *testing.T) {
@ -325,7 +327,7 @@ func TestConvertDNSConfigSearch(t *testing.T) {
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
Nameservers: nil,
Search: search,
}, dnsConfig))
}, dnsConfig, cmpopts.EquateComparable(netip.Addr{})))
}
func TestConvertCredentialSpec(t *testing.T) {
@ -514,8 +516,8 @@ func TestConvertServiceSecrets(t *testing.T) {
}
apiClient := &fakeClient{
secretListFunc: func(opts client.SecretListOptions) ([]swarm.Secret, error) {
assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_secret"))
assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_secret"))
assert.Check(t, opts.Filters["name"]["foo_secret"])
assert.Check(t, opts.Filters["name"]["bar_secret"])
return []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}},
@ -572,9 +574,9 @@ func TestConvertServiceConfigs(t *testing.T) {
}
apiClient := &fakeClient{
configListFunc: func(opts client.ConfigListOptions) ([]swarm.Config, error) {
assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_config"))
assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_config"))
assert.Check(t, is.Contains(opts.Filters.Get("name"), "baz_config"))
assert.Check(t, opts.Filters["name"]["foo_config"])
assert.Check(t, opts.Filters["name"]["bar_config"])
assert.Check(t, opts.Filters["name"]["baz_config"])
return []swarm.Config{
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}},
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}},

View File

@ -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
}