diff --git a/cli/command/builder/client_test.go b/cli/command/builder/client_test.go index 2568fca500af..f48914bdb76c 100644 --- a/cli/command/builder/client_test.go +++ b/cli/command/builder/client_test.go @@ -3,18 +3,17 @@ package builder import ( "context" - "github.com/moby/moby/api/types/build" "github.com/moby/moby/client" ) type fakeClient struct { client.Client - builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (*build.CachePruneReport, error) + builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) } -func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (*build.CachePruneReport, error) { +func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) { if c.builderPruneFunc != nil { return c.builderPruneFunc(ctx, opts) } - return nil, nil + return client.BuildCachePruneResult{}, nil } diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index 1619ef928364..2c5e0c8769fe 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -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") diff --git a/cli/command/builder/prune_test.go b/cli/command/builder/prune_test.go index c586c237c745..884523197e2b 100644 --- a/cli/command/builder/prune_test.go +++ b/cli/command/builder/prune_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/moby/moby/api/types/build" "github.com/moby/moby/client" ) @@ -16,8 +15,8 @@ func TestBuilderPromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (*build.CachePruneReport, error) { - return nil, errors.New("fakeClient builderPruneFunc should not be called") + builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) { + return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called") }, }) cmd := newPruneCommand(cli) diff --git a/cli/command/completion/functions_test.go b/cli/command/completion/functions_test.go index f651dde2f51d..db074f847c6b 100644 --- a/cli/command/completion/functions_test.go +++ b/cli/command/completion/functions_test.go @@ -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") } diff --git a/cli/command/config/ls_test.go b/cli/command/config/ls_test.go index e817c303f0bf..36ba8e1ecedd 100644 --- a/cli/command/config/ls_test.go +++ b/cli/command/config/ls_test.go @@ -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"), diff --git a/cli/command/container/client_test.go b/cli/command/container/client_test.go index f161906f7404..abcf26f1e94d 100644 --- a/cli/command/container/client_test.go +++ b/cli/command/container/client_test.go @@ -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) } diff --git a/cli/command/container/list_test.go b/cli/command/container/list_test.go index 742cf0fd54da..c815af85ab91 100644 --- a/cli/command/container/list_test.go +++ b/cli/command/container/list_test.go @@ -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)) } } diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go index cff4e499496a..46c41850a8a1 100644 --- a/cli/command/container/opts.go +++ b/cli/command/container/opts.go @@ -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 /[] // or /[] - proto, port := nat.SplitProtoPort(e) - // 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) + pr, err := network.ParsePortRange(e) if err != nil { - return nil, fmt.Errorf("invalid range format for --expose: %s, error: %w", e, err) + return nil, fmt.Errorf("invalid range format for --expose: %w", 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{}{} - } + // 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 + 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 +} diff --git a/cli/command/container/opts_test.go b/cli/command/container/opts_test.go index 874cbb903c60..68cbd7ef9df3 100644 --- a/cli/command/container/opts_test.go +++ b/cli/command/container/opts_test.go @@ -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{})) }) } } diff --git a/cli/command/container/port.go b/cli/command/container/port.go index 1df09417df9a..a10268b3bdf6 100644 --- a/cli/command/container/port.go +++ b/cli/command/container/port.go @@ -5,14 +5,13 @@ import ( "fmt" "net" "sort" - "strconv" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/fvbommel/sortorder" - "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/spf13/cobra" ) @@ -60,24 +59,21 @@ func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) erro var out []string if opts.port != "" { - port, proto, _ := strings.Cut(opts.port, "/") - if proto == "" { - proto = "tcp" + port, err := network.ParsePort(opts.port) + if err != nil { + return err } - if _, err = strconv.ParseUint(port, 10, 16); err != nil { - return fmt.Errorf("invalid port (%s): %w", port, err) - } - frontends, exists := c.NetworkSettings.Ports[container.PortRangeProto(port+"/"+proto)] + frontends, exists := c.NetworkSettings.Ports[port] if !exists || len(frontends) == 0 { return fmt.Errorf("no public port '%s' published for %s", opts.port, opts.container) } for _, frontend := range frontends { - out = append(out, net.JoinHostPort(frontend.HostIP, frontend.HostPort)) + out = append(out, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort)) } } else { for from, frontends := range c.NetworkSettings.Ports { for _, frontend := range frontends { - out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort))) + out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort))) } } } diff --git a/cli/command/container/port_test.go b/cli/command/container/port_test.go index e1a7e317ca2f..aaed4a03540c 100644 --- a/cli/command/container/port_test.go +++ b/cli/command/container/port_test.go @@ -2,10 +2,12 @@ package container import ( "io" + "net/netip" "testing" "github.com/docker/cli/internal/test" "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -13,32 +15,32 @@ import ( func TestNewPortCommandOutput(t *testing.T) { testCases := []struct { name string - ips []string + ips []netip.Addr port string }{ { name: "container-port-ipv4", - ips: []string{"0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("0.0.0.0")}, port: "80", }, { name: "container-port-ipv6", - ips: []string{"::"}, + ips: []netip.Addr{netip.MustParseAddr("::")}, port: "80", }, { name: "container-port-ipv6-and-ipv4", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, port: "80", }, { name: "container-port-ipv6-and-ipv4-443-udp", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, port: "443/udp", }, { name: "container-port-all-ports", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, }, } for _, tc := range testCases { @@ -46,19 +48,19 @@ func TestNewPortCommandOutput(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ inspectFunc: func(string) (container.InspectResponse, error) { ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}} - ci.NetworkSettings.Ports = container.PortMap{ - "80/tcp": make([]container.PortBinding, len(tc.ips)), - "443/tcp": make([]container.PortBinding, len(tc.ips)), - "443/udp": make([]container.PortBinding, len(tc.ips)), + ci.NetworkSettings.Ports = network.PortMap{ + network.MustParsePort("80/tcp"): make([]network.PortBinding, len(tc.ips)), + network.MustParsePort("443/tcp"): make([]network.PortBinding, len(tc.ips)), + network.MustParsePort("443/udp"): make([]network.PortBinding, len(tc.ips)), } for i, ip := range tc.ips { - ci.NetworkSettings.Ports["80/tcp"][i] = container.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("80/tcp")][i] = network.PortBinding{ HostIP: ip, HostPort: "3456", } - ci.NetworkSettings.Ports["443/tcp"][i] = container.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("443/tcp")][i] = network.PortBinding{ HostIP: ip, HostPort: "4567", } - ci.NetworkSettings.Ports["443/udp"][i] = container.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("443/udp")][i] = network.PortBinding{ HostIP: ip, HostPort: "5678", } } diff --git a/cli/command/container/prune_test.go b/cli/command/container/prune_test.go index 9227fe18f579..c7da1aea163f 100644 --- a/cli/command/container/prune_test.go +++ b/cli/command/container/prune_test.go @@ -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") }, }) diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go index a0f90bb135d7..eceab0c005a0 100644 --- a/cli/command/container/stats.go +++ b/cli/command/container/stats.go @@ -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") } diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 60dfb91b42f2..915b4a55b3fc 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -360,13 +360,13 @@ func DisplayablePorts(ports []container.PortSummary) string { for _, port := range ports { current := port.PrivatePort portKey := port.Type - if port.IP != "" { + if port.IP.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 { diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 594ced3b47d0..67c76cff97aa 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "fmt" + "net/netip" "strings" "testing" "time" @@ -660,7 +661,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "0.0.0.0", + IP: netip.MustParseAddr("0.0.0.0"), PrivatePort: 9988, Type: "tcp", }, @@ -670,7 +671,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::", + IP: netip.MustParseAddr("::"), PrivatePort: 9988, Type: "tcp", }, @@ -690,7 +691,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", @@ -701,7 +702,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", @@ -712,7 +713,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 9988, PublicPort: 9988, Type: "tcp", @@ -723,7 +724,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 9988, PublicPort: 9988, Type: "tcp", @@ -746,12 +747,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 9998, PrivatePort: 9998, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 9999, PrivatePort: 9999, Type: "udp", @@ -762,12 +763,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 9998, PrivatePort: 9998, Type: "udp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 9999, PrivatePort: 9999, Type: "udp", @@ -778,12 +779,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 8887, PrivatePort: 9998, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 8888, PrivatePort: 9999, Type: "udp", @@ -794,12 +795,12 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 8887, PrivatePort: 9998, Type: "udp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 8888, PrivatePort: 9999, Type: "udp", @@ -822,7 +823,7 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 6677, PublicPort: 7766, Type: "tcp", @@ -837,22 +838,22 @@ func TestDisplayablePorts(t *testing.T) { { ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 9988, PublicPort: 8899, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", }, { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", @@ -867,12 +868,12 @@ func TestDisplayablePorts(t *testing.T) { PublicPort: 8899, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 6677, PublicPort: 7766, Type: "tcp", }, { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", @@ -895,42 +896,42 @@ func TestDisplayablePorts(t *testing.T) { PrivatePort: 1024, Type: "udp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "tcp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "udp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "tcp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "udp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "tcp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "udp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "tcp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "udp", diff --git a/cli/command/image/client_test.go b/cli/command/image/client_test.go index de1f1b5679d1..376ac3b8035a 100644 --- a/cli/command/image/client_test.go +++ b/cli/command/image/client_test.go @@ -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) } diff --git a/cli/command/image/list_test.go b/cli/command/image/list_test.go index b3e36b7c3aad..52300beaacae 100644 --- a/cli/command/image/list_test.go +++ b/cli/command/image/list_test.go @@ -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 }, }, diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index 2c6d322a3e48..035e2c6e2410 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -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 { diff --git a/cli/command/image/prune_test.go b/cli/command/image/prune_test.go index d1543f71063e..d2a6cae09195 100644 --- a/cli/command/image/prune_test.go +++ b/cli/command/image/prune_test.go @@ -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") }, }) diff --git a/cli/command/image/pull_test.go b/cli/command/image/pull_test.go index dad36f21a62a..83a4aa98b524 100644 --- a/cli/command/image/pull_test.go +++ b/cli/command/image/pull_test.go @@ -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) diff --git a/cli/command/image/testdata/inspect-command-success.simple-many.golden b/cli/command/image/testdata/inspect-command-success.simple-many.golden index 6015d71e2b2f..d0f265403be9 100644 --- a/cli/command/image/testdata/inspect-command-success.simple-many.golden +++ b/cli/command/image/testdata/inspect-command-success.simple-many.golden @@ -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": "", diff --git a/cli/command/image/testdata/inspect-command-success.simple.golden b/cli/command/image/testdata/inspect-command-success.simple.golden index be54ee59a392..d2fee407957f 100644 --- a/cli/command/image/testdata/inspect-command-success.simple.golden +++ b/cli/command/image/testdata/inspect-command-success.simple.golden @@ -3,10 +3,6 @@ "Id": "", "RepoTags": null, "RepoDigests": null, - "Parent": "", - "Comment": "", - "DockerVersion": "", - "Author": "", "Config": null, "Architecture": "", "Os": "", diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go index caee86b3ab15..8d3101a76edd 100644 --- a/cli/command/image/tree.go +++ b/cli/command/image/tree.go @@ -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 { diff --git a/cli/command/network/client_test.go b/cli/command/network/client_test.go index b689d95d4cea..14834469f29b 100644 --- a/cli/command/network/client_test.go +++ b/cli/command/network/client_test.go @@ -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) } diff --git a/cli/command/network/connect.go b/cli/command/network/connect.go index dc9d96ae50f6..7f2be36ccf63 100644 --- a/cli/command/network/connect.go +++ b/cli/command/network/connect.go @@ -3,6 +3,8 @@ package network import ( "context" "errors" + "net" + "net/netip" "strings" "github.com/docker/cli/cli" @@ -17,11 +19,11 @@ import ( type connectOptions struct { network string container string - ipaddress string - ipv6address string + ipaddress net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly + ipv6address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly links opts.ListOpts aliases []string - linklocalips []string + linklocalips []net.IP // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly driverOpts []string gwPriority int } @@ -51,11 +53,11 @@ func newConnectCommand(dockerCLI command.Cli) *cobra.Command { } flags := cmd.Flags() - flags.StringVar(&options.ipaddress, "ip", "", `IPv4 address (e.g., "172.30.100.104")`) - flags.StringVar(&options.ipv6address, "ip6", "", `IPv6 address (e.g., "2001:db8::33")`) + flags.IPVar(&options.ipaddress, "ip", nil, `IPv4 address (e.g., "172.30.100.104")`) + flags.IPVar(&options.ipv6address, "ip6", nil, `IPv6 address (e.g., "2001:db8::33")`) flags.Var(&options.links, "link", "Add link to another container") flags.StringSliceVar(&options.aliases, "alias", []string{}, "Add network-scoped alias for the container") - flags.StringSliceVar(&options.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container") + flags.IPSliceVar(&options.linklocalips, "link-local-ip", nil, "Add a link-local address for the container") flags.StringSliceVar(&options.driverOpts, "driver-opt", []string{}, "driver options for the network") flags.IntVar(&options.gwPriority, "gw-priority", 0, "Highest gw-priority provides the default gateway. Accepts positive and negative values.") return cmd @@ -69,9 +71,9 @@ func runConnect(ctx context.Context, apiClient client.NetworkAPIClient, options return apiClient.NetworkConnect(ctx, options.network, options.container, &network.EndpointSettings{ IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: options.ipaddress, - IPv6Address: options.ipv6address, - LinkLocalIPs: options.linklocalips, + IPv4Address: toNetipAddr(options.ipaddress), + IPv6Address: toNetipAddr(options.ipv6address), + LinkLocalIPs: toNetipAddrSlice(options.linklocalips), }, Links: options.links.GetSlice(), Aliases: options.aliases, @@ -93,3 +95,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 +} diff --git a/cli/command/network/connect_test.go b/cli/command/network/connect_test.go index f1176a031ed4..e44bb8214324 100644 --- a/cli/command/network/connect_test.go +++ b/cli/command/network/connect_test.go @@ -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 }, }) diff --git a/cli/command/network/create.go b/cli/command/network/create.go index e0798584d42a..6ef9837ad640 100644 --- a/cli/command/network/create.go +++ b/cli/command/network/create.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net" + "net/netip" "strings" "github.com/docker/cli/cli" @@ -34,9 +35,9 @@ type createOptions struct { type ipamOptions struct { driver string - subnets []string - ipRanges []string - gateways []string + subnets []string // TODO(thaJeztah): change to []net.IPNet? This won't accept a bare address (without "/xxx"); we need a flag-type to handle []netip.Prefix directly + ipRanges []net.IPNet // TODO(thaJeztah): we need a flag-type to handle []netip.Prefix directly + gateways []net.IP // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly auxAddresses opts.MapOpts driverOpts opts.MapOpts } @@ -92,8 +93,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { flags.StringVar(&options.ipam.driver, "ipam-driver", "default", "IP Address Management Driver") flags.StringSliceVar(&options.ipam.subnets, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") - flags.StringSliceVar(&options.ipam.ipRanges, "ip-range", []string{}, "Allocate container ip from a sub-range") - flags.StringSliceVar(&options.ipam.gateways, "gateway", []string{}, "IPv4 or IPv6 Gateway for the master subnet") + flags.IPNetSliceVar(&options.ipam.ipRanges, "ip-range", nil, "Allocate container ip from a sub-range") + flags.IPSliceVar(&options.ipam.gateways, "gateway", nil, "IPv4 or IPv6 Gateway for the master subnet") flags.Var(&options.ipam.auxAddresses, "aux-address", "Auxiliary IPv4 or IPv6 addresses used by Network driver") flags.Var(&options.ipam.driverOpts, "ipam-opt", "Set IPAM driver specific options") @@ -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 { diff --git a/cli/command/network/create_test.go b/cli/command/network/create_test.go index a5d8c102ce00..50ae37bc567a 100644 --- a/cli/command/network/create_test.go +++ b/cli/command/network/create_test.go @@ -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()))) diff --git a/cli/command/network/list_test.go b/cli/command/network/list_test.go index 243dced61f73..e5dd899b0927 100644 --- a/cli/command/network/list_test.go +++ b/cli/command/network/list_test.go @@ -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"), diff --git a/cli/command/network/prune_test.go b/cli/command/network/prune_test.go index ee8780658603..ee476ee22668 100644 --- a/cli/command/network/prune_test.go +++ b/cli/command/network/prune_test.go @@ -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") }, }) diff --git a/cli/command/plugin/client_test.go b/cli/command/plugin/client_test.go index e59e5d8e0516..140d198f3428 100644 --- a/cli/command/plugin/client_test.go +++ b/cli/command/plugin/client_test.go @@ -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) } diff --git a/cli/command/plugin/completion.go b/cli/command/plugin/completion.go index 591c3b18260b..b44c61462cf7 100644 --- a/cli/command/plugin/completion.go +++ b/cli/command/plugin/completion.go @@ -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") diff --git a/cli/command/plugin/list_test.go b/cli/command/plugin/list_test.go index 86b8b92d414f..e4aa23cce1cc 100644 --- a/cli/command/plugin/list_test.go +++ b/cli/command/plugin/list_test.go @@ -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", diff --git a/cli/command/registry/search.go b/cli/command/registry/search.go index e1e7876332f0..a99631758ad2 100644 --- a/cli/command/registry/search.go +++ b/cli/command/registry/search.go @@ -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) diff --git a/cli/command/secret/ls_test.go b/cli/command/secret/ls_test.go index 8f98d22dbafd..18ecc2068f25 100644 --- a/cli/command/secret/ls_test.go +++ b/cli/command/secret/ls_test.go @@ -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"), diff --git a/cli/command/service/create_test.go b/cli/command/service/create_test.go index ad26af9a2a92..5e3ffaf3b67c 100644 --- a/cli/command/service/create_test.go +++ b/cli/command/service/create_test.go @@ -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{ { diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index ba00600a995c..778536a0d69a 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -735,7 +735,7 @@ type portRange struct { pEnd uint32 tStart uint32 tEnd uint32 - protocol swarm.PortConfigProtocol + protocol network.IPProtocol } func (pr portRange) String() string { diff --git a/cli/command/service/inspect_test.go b/cli/command/service/inspect_test.go index fa09a64ce6c6..ba9b8ee18ef3 100644 --- a/cli/command/service/inspect_test.go +++ b/cli/command/service/inspect_test.go @@ -6,6 +6,7 @@ package service import ( "bytes" "encoding/json" + "net/netip" "strings" "testing" "time" @@ -27,7 +28,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Mode: "vip", Ports: []swarm.PortConfig{ { - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, TargetPort: 5000, }, }, @@ -108,7 +109,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Spec: *endpointSpec, Ports: []swarm.PortConfig{ { - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, TargetPort: 5000, PublishedPort: 30000, }, @@ -116,7 +117,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) VirtualIPs: []swarm.EndpointVirtualIP{ { NetworkID: "6o4107cj2jx9tihgb0jyts6pj", - Addr: "10.255.0.4/16", + Addr: netip.MustParsePrefix("10.255.0.4/16"), // FIXME(thaJeztah): this was testing with "10.255.0.4/16" }, }, }, diff --git a/cli/command/service/list.go b/cli/command/service/list.go index 11f1388126d8..95254fd9c826 100644 --- a/cli/command/service/list.go +++ b/cli/command/service/list.go @@ -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. diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 2dc89fffcb92..4fa0036496b0 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "net/netip" "sort" "strconv" "strings" @@ -762,7 +763,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N Mounts: options.mounts.Value(), Init: &options.init, DNSConfig: &swarm.DNSConfig{ - Nameservers: options.dns.GetSlice(), + Nameservers: toNetipAddrSlice(options.dns.GetSlice()), Search: options.dnsSearch.GetSlice(), Options: options.dnsOption.GetSlice(), }, @@ -1073,3 +1074,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 +} diff --git a/cli/command/service/parse.go b/cli/command/service/parse.go index 9bc8bdb7d98b..f5f686f30bd1 100644 --- a/cli/command/service/parse.go +++ b/cli/command/service/parse.go @@ -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) } diff --git a/cli/command/service/progress/progress.go b/cli/command/service/progress/progress.go index b39a05e2a875..3c5be03b6e9f 100644 --- a/cli/command/service/progress/progress.go +++ b/cli/command/service/progress/progress.go @@ -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 } diff --git a/cli/command/service/ps.go b/cli/command/service/ps.go index c653339ff56a..6dbb36d9e99f 100644 --- a/cli/command/service/ps.go +++ b/cli/command/service/ps.go @@ -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) } } diff --git a/cli/command/service/ps_test.go b/cli/command/service/ps_test.go index fc6e3b2a8a8e..00a71456a363 100644 --- a/cli/command/service/ps_test.go +++ b/cli/command/service/ps_test.go @@ -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{}) diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 15e33419fa77..e4ffd4ceb785 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -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 { diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index 542c83adb5b6..52f5d366b99d 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "net/netip" "reflect" "sort" "testing" @@ -213,7 +214,7 @@ func TestUpdateDNSConfig(t *testing.T) { flags.Set("dns-option-rm", "timeout:3") config := &swarm.DNSConfig{ - Nameservers: []string{"3.3.3.3", "5.5.5.5"}, + Nameservers: []netip.Addr{netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("5.5.5.5")}, Search: []string{"localdomain"}, Options: []string{"timeout:3"}, } @@ -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) diff --git a/cli/command/stack/client_test.go b/cli/command/stack/client_test.go index 99d16f05e403..8c2c98f409b5 100644 --- a/cli/command/stack/client_test.go +++ b/cli/command/stack/client_test.go @@ -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+"=") } diff --git a/cli/command/stack/common.go b/cli/command/stack/common.go index 63ea675d1bcb..fc038aa59379 100644 --- a/cli/command/stack/common.go +++ b/cli/command/stack/common.go @@ -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) { diff --git a/cli/command/stack/services_test.go b/cli/command/stack/services_test.go index 51d2b8d38e20..0fe5b5fc8e0a 100644 --- a/cli/command/stack/services_test.go +++ b/cli/command/stack/services_test.go @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/client" "gotest.tools/v3/assert" @@ -164,7 +165,7 @@ func TestStackServicesWithoutFormat(t *testing.T) { PublishMode: swarm.PortConfigPublishModeIngress, PublishedPort: 0, TargetPort: 3232, - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, }), )}, nil }, diff --git a/cli/command/swarm/init.go b/cli/command/swarm/init.go index b19b1beb27c9..3c202649df61 100644 --- a/cli/command/swarm/init.go +++ b/cli/command/swarm/init.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" "strings" "github.com/docker/cli/cli" @@ -67,9 +68,22 @@ func newInitCommand(dockerCLI command.Cli) *cobra.Command { func runInit(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts initOptions) error { apiClient := dockerCLI.Client() - defaultAddrPool := make([]string, 0, len(opts.defaultAddrPools)) + // TODO(thaJeztah): 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(), diff --git a/cli/command/system/client_test.go b/cli/command/system/client_test.go index fff7d000ab04..be132374a99e 100644 --- a/cli/command/system/client_test.go +++ b/cli/command/system/client_test.go @@ -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) } diff --git a/cli/command/system/completion.go b/cli/command/system/completion.go index 544a0e029854..6a35ec364c36 100644 --- a/cli/command/system/completion.go +++ b/cli/command/system/completion.go @@ -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{} } diff --git a/cli/command/system/info.go b/cli/command/system/info.go index 0fcca3d95ef9..989359eea9bf 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -372,9 +372,8 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { } } - for _, registryConfig := range info.RegistryConfig.InsecureRegistryCIDRs { - mask, _ := registryConfig.Mask.Size() - fprintf(output, " %s/%d\n", registryConfig.IP.String(), mask) + for _, cidr := range info.RegistryConfig.InsecureRegistryCIDRs { + fprintf(output, " %s\n", cidr) } } @@ -429,7 +428,7 @@ func printSwarmInfo(output io.Writer, info system.Info) { var strAddrPool strings.Builder if info.Swarm.Cluster.DefaultAddrPool != nil { for _, p := range info.Swarm.Cluster.DefaultAddrPool { - strAddrPool.WriteString(p + " ") + strAddrPool.WriteString(p.String() + " ") } fprintln(output, " Default Address Pool:", strAddrPool.String()) fprintln(output, " SubnetSize:", info.Swarm.Cluster.SubnetSize) diff --git a/cli/command/system/info_test.go b/cli/command/system/info_test.go index 90222a61699f..09278ee206e8 100644 --- a/cli/command/system/info_test.go +++ b/cli/command/system/info_test.go @@ -3,7 +3,7 @@ package system import ( "encoding/base64" "errors" - "net" + "net/netip" "testing" "time" @@ -69,11 +69,8 @@ var sampleInfoNoSwarm = system.Info{ Architecture: "x86_64", IndexServerAddress: "https://index.docker.io/v1/", RegistryConfig: ®istrytypes.ServiceConfig{ - InsecureRegistryCIDRs: []*registrytypes.NetIPNet{ - { - IP: net.ParseIP("127.0.0.0"), - Mask: net.IPv4Mask(255, 0, 0, 0), - }, + InsecureRegistryCIDRs: []netip.Prefix{ + netip.MustParsePrefix("127.0.0.0/8"), }, IndexConfigs: map[string]*registrytypes.IndexInfo{ "docker.io": { @@ -119,7 +116,7 @@ var sampleInfoNoSwarm = system.Info{ SecurityOptions: []string{"name=apparmor", "name=seccomp,profile=default"}, DefaultAddressPools: []system.NetworkAddressPool{ { - Base: "10.123.0.0/16", + Base: netip.MustParsePrefix("10.123.0.0/16"), Size: 24, }, }, diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index 37469395875e..1872c807bf98 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -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) } } diff --git a/cli/command/system/prune_test.go b/cli/command/system/prune_test.go index 3b47c4d45586..b8c2f23f2e06 100644 --- a/cli/command/system/prune_test.go +++ b/cli/command/system/prune_test.go @@ -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") }, }) diff --git a/cli/command/utils.go b/cli/command/utils.go index 6642d76a20b1..defef142041c 100644 --- a/cli/command/utils.go +++ b/cli/command/utils.go @@ -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)) diff --git a/cli/command/volume/client_test.go b/cli/command/volume/client_test.go index 5e611cfd67aa..9e87ee37a77a 100644 --- a/cli/command/volume/client_test.go +++ b/cli/command/volume/client_test.go @@ -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) } diff --git a/cli/command/volume/list_test.go b/cli/command/volume/list_test.go index a90d96e8e457..ff08037e2d06 100644 --- a/cli/command/volume/list_test.go +++ b/cli/command/volume/list_test.go @@ -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{ { diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go index 9d17a7868e37..9b9ffdf805c8 100644 --- a/cli/command/volume/prune.go +++ b/cli/command/volume/prune.go @@ -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 { diff --git a/cli/command/volume/prune_test.go b/cli/command/volume/prune_test.go index 6af4906dd45b..6a30d40a0bcf 100644 --- a/cli/command/volume/prune_test.go +++ b/cli/command/volume/prune_test.go @@ -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") }, }) diff --git a/cli/compose/convert/compose.go b/cli/compose/convert/compose.go index d2122f8b26b3..4062bbb7c096 100644 --- a/cli/compose/convert/compose.go +++ b/cli/compose/convert/compose.go @@ -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, }) } } diff --git a/cli/compose/convert/compose_test.go b/cli/compose/convert/compose_test.go index f3d697b6725d..afd7420576ef 100644 --- a/cli/compose/convert/compose_test.go +++ b/cli/compose/convert/compose_test.go @@ -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) } diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 9714cb372f47..5ae8e122e9a7 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/netip" "os" "sort" "strings" @@ -13,6 +14,7 @@ import ( composetypes "github.com/docker/cli/cli/compose/types" "github.com/docker/cli/opts" "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/api/types/versions" "github.com/moby/moby/client" @@ -96,7 +98,7 @@ func Service( return swarm.ServiceSpec{}, err } - dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch) + dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch) // TODO(thaJeztah): change service.DNS to a []netip.Addr var privileges swarm.Privileges privileges.CredentialSpec, err = convertCredentialSpec( @@ -578,7 +580,7 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC portConfigs := []swarm.PortConfig{} for _, port := range source { portConfig := swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(port.Protocol), + Protocol: network.IPProtocol(port.Protocol), TargetPort: port.Target, PublishedPort: port.Published, PublishMode: swarm.PortConfigPublishMode(port.Mode), @@ -641,15 +643,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 diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index 82db561577e1..165e5bf99c4c 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -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"}}}, diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index 953bdd4f3d23..92fff4fa277c 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -25,7 +25,7 @@ import ( "github.com/docker/go-units" "github.com/go-viper/mapstructure/v2" "github.com/google/shlex" - "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/versions" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -925,7 +925,7 @@ func toServicePortConfigs(value string) ([]any, error) { return nil, err } // We need to sort the key of the ports to make sure it is consistent - keys := []string{} + keys := make([]string, 0, len(ports)) for port := range ports { keys = append(keys, string(port)) } @@ -933,7 +933,11 @@ func toServicePortConfigs(value string) ([]any, error) { for _, key := range keys { // Reuse ConvertPortToPortConfig so that it is consistent - portConfig, err := swarmopts.ConvertPortToPortConfig(container.PortRangeProto(key), portBindings) + port, err := network.ParsePort(key) + if err != nil { + return nil, err + } + portConfig, err := swarmopts.ConvertPortToPortConfig(port, portBindings) if err != nil { return nil, err } diff --git a/docs/reference/commandline/builder_prune.md b/docs/reference/commandline/builder_prune.md index 8dbbfbd8d220..2d809b0de3fc 100644 --- a/docs/reference/commandline/builder_prune.md +++ b/docs/reference/commandline/builder_prune.md @@ -8,7 +8,7 @@ Remove build cache | Name | Type | Default | Description | |:-----------------|:---------|:--------|:------------------------------------------------------| | `-a`, `--all` | `bool` | | Remove all unused build cache, not just dangling ones | -| `--filter` | `filter` | | Provide filter values (e.g. `until=24h`) | +| `--filter` | `filter` | `{}` | Provide filter values (e.g. `until=24h`) | | `-f`, `--force` | `bool` | | Do not prompt for confirmation | | `--keep-storage` | `bytes` | `0` | Amount of disk space to keep for cache | diff --git a/docs/reference/commandline/config_ls.md b/docs/reference/commandline/config_ls.md index 1f5ad9e844ab..8e0d44b1cb99 100644 --- a/docs/reference/commandline/config_ls.md +++ b/docs/reference/commandline/config_ls.md @@ -11,7 +11,7 @@ List configs | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-q`, `--quiet` | `bool` | | Only display IDs | diff --git a/docs/reference/commandline/container_create.md b/docs/reference/commandline/container_create.md index 404657cbdba8..e22bea253fc9 100644 --- a/docs/reference/commandline/container_create.md +++ b/docs/reference/commandline/container_create.md @@ -60,8 +60,8 @@ Create a new container | `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | `--ipc` | `string` | | IPC mode to use | | `--isolation` | `string` | | Container isolation technology | | `-l`, `--label` | `list` | | Set meta data on a container | diff --git a/docs/reference/commandline/container_ls.md b/docs/reference/commandline/container_ls.md index a19f7a5e0410..d94eda7d8403 100644 --- a/docs/reference/commandline/container_ls.md +++ b/docs/reference/commandline/container_ls.md @@ -12,7 +12,7 @@ List containers | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-a`](#all), [`--all`](#all) | `bool` | | Show all containers (default shows just running) | -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-n`, `--last` | `int` | `-1` | Show n last created containers (includes all states) | | `-l`, `--latest` | `bool` | | Show the latest created container (includes all states) | diff --git a/docs/reference/commandline/container_prune.md b/docs/reference/commandline/container_prune.md index dd753aa26b8d..4c16c3091978 100644 --- a/docs/reference/commandline/container_prune.md +++ b/docs/reference/commandline/container_prune.md @@ -7,7 +7,7 @@ Remove all stopped containers | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------| -| [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `until=`) | +| [`--filter`](#filter) | `filter` | `{}` | Provide filter values (e.g. `until=`) | | `-f`, `--force` | `bool` | | Do not prompt for confirmation | diff --git a/docs/reference/commandline/container_run.md b/docs/reference/commandline/container_run.md index cd47b0eb9851..41aaba0ff9a9 100644 --- a/docs/reference/commandline/container_run.md +++ b/docs/reference/commandline/container_run.md @@ -62,8 +62,8 @@ Create and run a new container from an image | [`-i`](#interactive), [`--interactive`](#interactive) | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | [`--ipc`](#ipc) | `string` | | IPC mode to use | | [`--isolation`](#isolation) | `string` | | Container isolation technology | | [`-l`](#label), [`--label`](#label) | `list` | | Set meta data on a container | diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index c4ce24fa32b8..24867406acac 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -60,8 +60,8 @@ Create a new container | `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | `--ipc` | `string` | | IPC mode to use | | `--isolation` | `string` | | Container isolation technology | | `-l`, `--label` | `list` | | Set meta data on a container | diff --git a/docs/reference/commandline/events.md b/docs/reference/commandline/events.md index a5012cd6b7c3..0ca52a5ad68b 100644 --- a/docs/reference/commandline/events.md +++ b/docs/reference/commandline/events.md @@ -11,7 +11,7 @@ Get real time events from the server | Name | Type | Default | Description | |:-----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-f`, `--filter` | `filter` | | Filter output based on conditions provided | +| `-f`, `--filter` | `filter` | `{}` | Filter output based on conditions provided | | `--format` | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `--since` | `string` | | Show all events created since timestamp | | `--until` | `string` | | Stream events until this timestamp | diff --git a/docs/reference/commandline/image_ls.md b/docs/reference/commandline/image_ls.md index 925b6993bccd..1124790c0088 100644 --- a/docs/reference/commandline/image_ls.md +++ b/docs/reference/commandline/image_ls.md @@ -13,7 +13,7 @@ List images |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-a`, `--all` | `bool` | | Show all images (default hides intermediate images) | | [`--digests`](#digests) | `bool` | | Show digests | -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output | | `-q`, `--quiet` | `bool` | | Only show image IDs | diff --git a/docs/reference/commandline/image_prune.md b/docs/reference/commandline/image_prune.md index bd02b16275d7..2954fdce33b2 100644 --- a/docs/reference/commandline/image_prune.md +++ b/docs/reference/commandline/image_prune.md @@ -8,7 +8,7 @@ Remove unused images | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------| | `-a`, `--all` | `bool` | | Remove all unused images, not just dangling ones | -| [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `until=`) | +| [`--filter`](#filter) | `filter` | `{}` | Provide filter values (e.g. `until=`) | | `-f`, `--force` | `bool` | | Do not prompt for confirmation | diff --git a/docs/reference/commandline/images.md b/docs/reference/commandline/images.md index 8d615ec21b10..be20d2410305 100644 --- a/docs/reference/commandline/images.md +++ b/docs/reference/commandline/images.md @@ -13,7 +13,7 @@ List images |:-----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-a`, `--all` | `bool` | | Show all images (default hides intermediate images) | | `--digests` | `bool` | | Show digests | -| `-f`, `--filter` | `filter` | | Filter output based on conditions provided | +| `-f`, `--filter` | `filter` | `{}` | Filter output based on conditions provided | | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `--no-trunc` | `bool` | | Don't truncate output | | `-q`, `--quiet` | `bool` | | Only show image IDs | diff --git a/docs/reference/commandline/network_connect.md b/docs/reference/commandline/network_connect.md index 3a6514fab719..c3e373d5e867 100644 --- a/docs/reference/commandline/network_connect.md +++ b/docs/reference/commandline/network_connect.md @@ -10,10 +10,10 @@ Connect a container to a network | [`--alias`](#alias) | `stringSlice` | | Add network-scoped alias for the container | | `--driver-opt` | `stringSlice` | | driver options for the network | | `--gw-priority` | `int` | `0` | Highest gw-priority provides the default gateway. Accepts positive and negative values. | -| [`--ip`](#ip) | `string` | | IPv4 address (e.g., `172.30.100.104`) | -| `--ip6` | `string` | | IPv6 address (e.g., `2001:db8::33`) | +| [`--ip`](#ip) | `ip` | `` | IPv4 address (e.g., `172.30.100.104`) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., `2001:db8::33`) | | [`--link`](#link) | `list` | | Add link to another container | -| `--link-local-ip` | `stringSlice` | | Add a link-local address for the container | +| `--link-local-ip` | `ipSlice` | | Add a link-local address for the container | diff --git a/docs/reference/commandline/network_create.md b/docs/reference/commandline/network_create.md index 4c36adf9f487..ac493a844e10 100644 --- a/docs/reference/commandline/network_create.md +++ b/docs/reference/commandline/network_create.md @@ -12,10 +12,10 @@ Create a network | `--config-from` | `string` | | The network from which to copy the configuration | | `--config-only` | `bool` | | Create a configuration only network | | `-d`, `--driver` | `string` | `bridge` | Driver to manage the Network | -| `--gateway` | `stringSlice` | | IPv4 or IPv6 Gateway for the master subnet | +| `--gateway` | `ipSlice` | | IPv4 or IPv6 Gateway for the master subnet | | [`--ingress`](#ingress) | `bool` | | Create swarm routing-mesh network | | [`--internal`](#internal) | `bool` | | Restrict external access to the network | -| `--ip-range` | `stringSlice` | | Allocate container ip from a sub-range | +| `--ip-range` | `ipNetSlice` | | Allocate container ip from a sub-range | | `--ipam-driver` | `string` | `default` | IP Address Management Driver | | `--ipam-opt` | `map` | `map[]` | Set IPAM driver specific options | | `--ipv4` | `bool` | `true` | Enable or disable IPv4 address assignment | diff --git a/docs/reference/commandline/network_ls.md b/docs/reference/commandline/network_ls.md index ff3b4001cc19..49a8fe284e25 100644 --- a/docs/reference/commandline/network_ls.md +++ b/docs/reference/commandline/network_ls.md @@ -11,7 +11,7 @@ List networks | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `driver=bridge`) | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Provide filter values (e.g. `driver=bridge`) | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | [`--no-trunc`](#no-trunc) | `bool` | | Do not truncate the output | | `-q`, `--quiet` | `bool` | | Only display network IDs | diff --git a/docs/reference/commandline/network_prune.md b/docs/reference/commandline/network_prune.md index 4ee4101faf25..e078d5ed0c5b 100644 --- a/docs/reference/commandline/network_prune.md +++ b/docs/reference/commandline/network_prune.md @@ -7,7 +7,7 @@ Remove all unused networks | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------| -| [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `until=`) | +| [`--filter`](#filter) | `filter` | `{}` | Provide filter values (e.g. `until=`) | | `-f`, `--force` | `bool` | | Do not prompt for confirmation | diff --git a/docs/reference/commandline/node_ls.md b/docs/reference/commandline/node_ls.md index 1a8b42de6477..371e05250171 100644 --- a/docs/reference/commandline/node_ls.md +++ b/docs/reference/commandline/node_ls.md @@ -11,7 +11,7 @@ List nodes in the swarm | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-q`, `--quiet` | `bool` | | Only display IDs | diff --git a/docs/reference/commandline/node_ps.md b/docs/reference/commandline/node_ps.md index fdd21f4f9986..b3120c70a445 100644 --- a/docs/reference/commandline/node_ps.md +++ b/docs/reference/commandline/node_ps.md @@ -7,7 +7,7 @@ List tasks running on one or more nodes, defaults to current node | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Pretty-print tasks using a Go template | | `--no-resolve` | `bool` | | Do not map IDs to Names | | `--no-trunc` | `bool` | | Do not truncate output | diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md index 036aad76c505..4610c262aa09 100644 --- a/docs/reference/commandline/plugin_ls.md +++ b/docs/reference/commandline/plugin_ls.md @@ -11,7 +11,7 @@ List plugins | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `enabled=true`) | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Provide filter values (e.g. `enabled=true`) | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `--no-trunc` | `bool` | | Don't truncate output | | `-q`, `--quiet` | `bool` | | Only display plugin IDs | diff --git a/docs/reference/commandline/ps.md b/docs/reference/commandline/ps.md index 5d1cf16fcf6e..1560dbdee956 100644 --- a/docs/reference/commandline/ps.md +++ b/docs/reference/commandline/ps.md @@ -12,7 +12,7 @@ List containers | Name | Type | Default | Description | |:-----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-a`, `--all` | `bool` | | Show all containers (default shows just running) | -| `-f`, `--filter` | `filter` | | Filter output based on conditions provided | +| `-f`, `--filter` | `filter` | `{}` | Filter output based on conditions provided | | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-n`, `--last` | `int` | `-1` | Show n last created containers (includes all states) | | `-l`, `--latest` | `bool` | | Show the latest created container (includes all states) | diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index 2ef7e6fc78d8..dcc5cd24bf67 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -62,8 +62,8 @@ Create and run a new container from an image | `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | `--ipc` | `string` | | IPC mode to use | | `--isolation` | `string` | | Container isolation technology | | `-l`, `--label` | `list` | | Set meta data on a container | diff --git a/docs/reference/commandline/search.md b/docs/reference/commandline/search.md index 03c98eab1bed..82847a14d4f8 100644 --- a/docs/reference/commandline/search.md +++ b/docs/reference/commandline/search.md @@ -7,7 +7,7 @@ Search Docker Hub for images | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Pretty-print search using a Go template | | [`--limit`](#limit) | `int` | `0` | Max number of search results | | [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output | diff --git a/docs/reference/commandline/secret_ls.md b/docs/reference/commandline/secret_ls.md index 2137bbc7e269..f911b240e6f2 100644 --- a/docs/reference/commandline/secret_ls.md +++ b/docs/reference/commandline/secret_ls.md @@ -11,7 +11,7 @@ List secrets | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-q`, `--quiet` | `bool` | | Only display IDs | diff --git a/docs/reference/commandline/service_ls.md b/docs/reference/commandline/service_ls.md index 9cb4d08d84c6..8c5773a1ef37 100644 --- a/docs/reference/commandline/service_ls.md +++ b/docs/reference/commandline/service_ls.md @@ -11,7 +11,7 @@ List services | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-q`, `--quiet` | `bool` | | Only display IDs | diff --git a/docs/reference/commandline/service_ps.md b/docs/reference/commandline/service_ps.md index 0cd1d4360c7b..1226aadf50cb 100644 --- a/docs/reference/commandline/service_ps.md +++ b/docs/reference/commandline/service_ps.md @@ -7,7 +7,7 @@ List the tasks of one or more services | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Pretty-print tasks using a Go template | | `--no-resolve` | `bool` | | Do not map IDs to Names | | `--no-trunc` | `bool` | | Do not truncate output | diff --git a/docs/reference/commandline/stack_ps.md b/docs/reference/commandline/stack_ps.md index 1fa10a286168..a590d054c028 100644 --- a/docs/reference/commandline/stack_ps.md +++ b/docs/reference/commandline/stack_ps.md @@ -7,7 +7,7 @@ List the tasks in the stack | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | [`--no-resolve`](#no-resolve) | `bool` | | Do not map IDs to Names | | [`--no-trunc`](#no-trunc) | `bool` | | Do not truncate output | diff --git a/docs/reference/commandline/stack_services.md b/docs/reference/commandline/stack_services.md index e940207b051f..fbea5678f6a3 100644 --- a/docs/reference/commandline/stack_services.md +++ b/docs/reference/commandline/stack_services.md @@ -7,7 +7,7 @@ List the services in the stack | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-q`, `--quiet` | `bool` | | Only display IDs | diff --git a/docs/reference/commandline/system_events.md b/docs/reference/commandline/system_events.md index 7de1fb61f6b9..3926a23eca1b 100644 --- a/docs/reference/commandline/system_events.md +++ b/docs/reference/commandline/system_events.md @@ -11,7 +11,7 @@ Get real time events from the server | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | [`--since`](#since) | `string` | | Show all events created since timestamp | | `--until` | `string` | | Stream events until this timestamp | diff --git a/docs/reference/commandline/system_prune.md b/docs/reference/commandline/system_prune.md index 39a58c54996c..04698d7fc917 100644 --- a/docs/reference/commandline/system_prune.md +++ b/docs/reference/commandline/system_prune.md @@ -8,7 +8,7 @@ Remove unused data | Name | Type | Default | Description | |:----------------------|:---------|:--------|:---------------------------------------------------| | `-a`, `--all` | `bool` | | Remove all unused images not just dangling ones | -| [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `label==`) | +| [`--filter`](#filter) | `filter` | `{}` | Provide filter values (e.g. `label==`) | | `-f`, `--force` | `bool` | | Do not prompt for confirmation | | `--volumes` | `bool` | | Prune anonymous volumes | diff --git a/docs/reference/commandline/volume_ls.md b/docs/reference/commandline/volume_ls.md index 8420080a7347..d35b5a15b3c0 100644 --- a/docs/reference/commandline/volume_ls.md +++ b/docs/reference/commandline/volume_ls.md @@ -12,7 +12,7 @@ List volumes | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--cluster` | `bool` | | Display only cluster volumes, and use cluster volume list formatting | -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `dangling=true`) | +| [`-f`](#filter), [`--filter`](#filter) | `filter` | `{}` | Provide filter values (e.g. `dangling=true`) | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-q`, `--quiet` | `bool` | | Only display volume names | diff --git a/docs/reference/commandline/volume_prune.md b/docs/reference/commandline/volume_prune.md index b0aa7061c524..f276ed59e2ed 100644 --- a/docs/reference/commandline/volume_prune.md +++ b/docs/reference/commandline/volume_prune.md @@ -8,7 +8,7 @@ Remove unused local volumes | Name | Type | Default | Description | |:------------------------------|:---------|:--------|:---------------------------------------------------| | [`-a`](#all), [`--all`](#all) | `bool` | | Remove all unused volumes, not just anonymous ones | -| [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `label=