Merge pull request #50860 from corhere/network-filter-iface

daemon: filter networks before converting to API types
This commit is contained in:
Cory Snider
2025-09-04 13:48:07 -04:00
committed by GitHub
13 changed files with 611 additions and 356 deletions

View File

@@ -2,8 +2,6 @@ package network
import (
"time"
"github.com/moby/moby/api/types/filters"
)
const (
@@ -116,21 +114,6 @@ type ConfigReference struct {
Network string
}
var acceptedFilters = map[string]bool{
"dangling": true,
"driver": true,
"id": true,
"label": true,
"name": true,
"scope": true,
"type": true,
}
// ValidateFilters validates the list of filter args with the available filters.
func ValidateFilters(filter filters.Args) error {
return filter.Validate(acceptedFilters)
}
// PruneReport contains the response for Engine API:
// POST "/networks/prune"
type PruneReport struct {

View File

@@ -1,9 +1,9 @@
package daemon
import (
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
lncluster "github.com/moby/moby/v2/daemon/libnetwork/cluster"
dnetwork "github.com/moby/moby/v2/daemon/network"
)
// Cluster is the interface for [github.com/moby/moby/v2/daemon/cluster.Cluster].
@@ -22,6 +22,6 @@ type ClusterStatus interface {
// NetworkManager provides methods to manage networks
type NetworkManager interface {
GetNetwork(input string) (network.Inspect, error)
GetNetworks(filters.Args) ([]network.Inspect, error)
GetNetworks(dnetwork.Filter) ([]network.Inspect, error)
RemoveNetwork(input string) error
}

View File

@@ -2,6 +2,7 @@ package convert
import (
"strings"
"time"
gogotypes "github.com/gogo/protobuf/types"
"github.com/moby/moby/api/types/network"
@@ -240,3 +241,47 @@ func IsIngressNetwork(n *swarmapi.Network) bool {
_, ok := n.Spec.Annotations.Labels["com.docker.swarm.internal"]
return ok && n.Spec.Annotations.Name == "ingress"
}
// FilterNetwork adapts [swarmapi.Network] to the
// [github.com/moby/moby/v2/daemon/network.FilterNetwork] interface.
type FilterNetwork struct {
N *swarmapi.Network
}
func (nw FilterNetwork) ID() string {
return nw.N.ID
}
func (nw FilterNetwork) Name() string {
return nw.N.Spec.Annotations.Name
}
func (nw FilterNetwork) Driver() string {
if nw.N.DriverState != nil {
return nw.N.DriverState.Name
}
return ""
}
func (nw FilterNetwork) Labels() map[string]string {
return nw.N.Spec.Annotations.Labels
}
func (nw FilterNetwork) Scope() string {
return scope.Swarm
}
func (nw FilterNetwork) Created() time.Time {
t, _ := gogotypes.TimestampFromProto(nw.N.Meta.CreatedAt)
return t
}
func (nw FilterNetwork) HasContainerAttachments() bool {
// Not tracked in swarmkit
return false
}
func (nw FilterNetwork) HasServiceAttachments() bool {
// Not tracked in swarmkit
return false
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/containerd/log"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
types "github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/v2/daemon/cluster/convert"
@@ -16,65 +15,45 @@ import (
)
// GetNetworks returns all current cluster managed networks.
func (c *Cluster) GetNetworks(filter filters.Args) ([]network.Inspect, error) {
var f *swarmapi.ListNetworksRequest_Filters
if filter.Len() > 0 {
f = &swarmapi.ListNetworksRequest_Filters{}
if filter.Contains("name") {
f.Names = filter.Get("name")
f.NamePrefixes = filter.Get("name")
}
if filter.Contains("id") {
f.IDPrefixes = filter.Get("id")
}
}
list, err := c.getNetworks(f)
func (c *Cluster) GetNetworks(filter networkSettings.Filter) ([]network.Inspect, error) {
// Swarmkit API's filters are too limited to express the Moby filter
// semantics with much fidelity. It only supports filtering on one of:
// - Names (exact match)
// - NamePrefixes (prefix match)
// - IDPrefixes (prefix match)
// The first of the list that is set is used as the filter predicate.
// The other fields are ignored. However, the Engine API filter
// semantics are to match on any substring of the network name or ID. We
// therefore need to request all networks from Swarmkit and filter them
// ourselves.
list, err := c.listNetworks(context.TODO(), nil)
if err != nil {
return nil, err
}
filterPredefinedNetworks(&list)
return networkSettings.FilterNetworks(list, filter)
}
func filterPredefinedNetworks(networks *[]network.Inspect) {
if networks == nil {
return
}
var idxs []int
for i, nw := range *networks {
if v, ok := nw.Labels["com.docker.swarm.predefined"]; ok && v == "true" {
idxs = append(idxs, i)
var filtered []network.Inspect
for _, n := range list {
if n.Spec.Annotations.Labels["com.docker.swarm.predefined"] == "true" {
continue
}
if filter.Matches(convert.FilterNetwork{N: n}) {
filtered = append(filtered, convert.BasicNetworkFromGRPC(*n))
}
}
for i, idx := range idxs {
idx -= i
*networks = append((*networks)[:idx], (*networks)[idx+1:]...)
}
return filtered, nil
}
func (c *Cluster) getNetworks(filters *swarmapi.ListNetworksRequest_Filters) ([]network.Inspect, error) {
var r *swarmapi.ListNetworksResponse
err := c.lockedManagerAction(context.TODO(), func(ctx context.Context, state nodeState) error {
var err error
r, err = state.controlClient.ListNetworks(ctx, &swarmapi.ListNetworksRequest{Filters: filters})
return err
func (c *Cluster) listNetworks(ctx context.Context, filters *swarmapi.ListNetworksRequest_Filters) ([]*swarmapi.Network, error) {
var list []*swarmapi.Network
err := c.lockedManagerAction(ctx, func(ctx context.Context, state nodeState) error {
l, err := state.controlClient.ListNetworks(ctx, &swarmapi.ListNetworksRequest{Filters: filters})
if err != nil {
return err
}
list = l.Networks
return nil
})
if err != nil {
return nil, err
}
networks := make([]network.Inspect, 0, len(r.Networks))
for _, nw := range r.Networks {
networks = append(networks, convert.BasicNetworkFromGRPC(*nw))
}
return networks, nil
return list, err
}
// GetNetwork returns a cluster network by an ID.
@@ -99,9 +78,17 @@ func (c *Cluster) GetNetwork(input string) (network.Inspect, error) {
func (c *Cluster) GetNetworksByName(name string) ([]network.Inspect, error) {
// Note that swarmapi.GetNetworkRequest.Name is not functional.
// So we cannot just use that with c.GetNetwork.
return c.getNetworks(&swarmapi.ListNetworksRequest_Filters{
list, err := c.listNetworks(context.TODO(), &swarmapi.ListNetworksRequest_Filters{
Names: []string{name},
})
if err != nil {
return nil, err
}
nr := make([]network.Inspect, len(list))
for i, n := range list {
nr[i] = convert.BasicNetworkFromGRPC(*n)
}
return nr, nil
}
func attacherKey(target, containerID string) string {

View File

@@ -539,6 +539,11 @@ func (n *Network) Services() map[string]ServiceInfo {
return sinfo
}
// HasServiceAttachments returns true when len(n.Services()) > 0.
func (n *Network) HasServiceAttachments() bool {
return len(n.Services()) > 0
}
// clusterAgent returns the cluster agent if the network is a swarm-scoped,
// multi-host network.
func (n *Network) clusterAgent() (agent *nwAgent, ok bool) {

View File

@@ -249,6 +249,11 @@ func (n *Network) Type() string {
return n.networkType
}
// Driver is an alias for [Network.Type].
func (n *Network) Driver() string {
return n.Type()
}
func (n *Network) Resolvers() []*Resolver {
n.mu.Lock()
defer n.mu.Unlock()
@@ -1283,6 +1288,11 @@ func (n *Network) Endpoints() []*Endpoint {
return endpoints
}
// HasContainerAttachments returns true when len(n.Endpoints()) > 0.
func (n *Network) HasContainerAttachments() bool {
return len(n.Endpoints()) > 0
}
// WalkEndpoints uses the provided function to walk the Endpoints.
func (n *Network) WalkEndpoints(walker EndpointWalker) {
for _, e := range n.Endpoints() {

View File

@@ -17,7 +17,6 @@ import (
"github.com/docker/go-connections/nat"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/filters"
networktypes "github.com/moby/moby/api/types/network"
clustertypes "github.com/moby/moby/v2/daemon/cluster/provider"
"github.com/moby/moby/v2/daemon/config"
@@ -591,34 +590,19 @@ func (daemon *Daemon) deleteNetwork(nw *libnetwork.Network, dynamic bool) error
}
// GetNetworks returns a list of all networks
func (daemon *Daemon) GetNetworks(filter filters.Args, config backend.NetworkListConfig) ([]networktypes.Inspect, error) {
var idx map[string]*libnetwork.Network
if config.Detailed {
idx = make(map[string]*libnetwork.Network)
}
func (daemon *Daemon) GetNetworks(filter network.Filter, config backend.NetworkListConfig) ([]networktypes.Inspect, error) {
allNetworks := daemon.getAllNetworks()
networks := make([]networktypes.Inspect, 0, len(allNetworks))
for _, n := range allNetworks {
nr := buildNetworkResource(n)
networks = append(networks, nr)
if config.Detailed {
idx[nr.ID] = n
}
}
var err error
networks, err = network.FilterNetworks(networks, filter)
if err != nil {
return nil, err
}
if config.Detailed {
for i, nw := range networks {
networks[i].Containers = buildContainerAttachments(idx[nw.ID])
if config.Verbose {
networks[i].Services = buildServiceAttachments(idx[nw.ID])
if filter.Matches(n) {
nr := buildNetworkResource(n)
if config.Detailed {
nr.Containers = buildContainerAttachments(n)
if config.Verbose {
nr.Services = buildServiceAttachments(n)
}
}
networks = append(networks, nr)
}
}

View File

@@ -1,129 +1,194 @@
package network
import (
"time"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"github.com/moby/moby/v2/errdefs"
"github.com/pkg/errors"
)
// FilterNetworks filters network list according to user specified filter
// and returns user chosen networks
func FilterNetworks(nws []network.Inspect, filter filters.Args) ([]network.Inspect, error) {
// if filter is empty, return original network list
if filter.Len() == 0 {
return nws, nil
var (
acceptedFilters = map[string]bool{
"dangling": true,
"driver": true,
"id": true,
"label": true,
"name": true,
"scope": true,
"type": true,
}
displayNet := nws[:0]
for _, nw := range nws {
if filter.Contains("driver") {
if !filter.ExactMatch("driver", nw.Driver) {
continue
}
}
if filter.Contains("name") {
if !filter.Match("name", nw.Name) {
continue
}
}
if filter.Contains("id") {
if !filter.Match("id", nw.ID) {
continue
}
}
if filter.Contains("label") {
if !filter.MatchKVList("label", nw.Labels) {
continue
}
}
if filter.Contains("scope") {
if !filter.ExactMatch("scope", nw.Scope) {
continue
}
}
if filter.Contains("idOrName") {
if !filter.Match("name", nw.Name) && !filter.Match("id", nw.Name) {
continue
}
}
displayNet = append(displayNet, nw)
pruneFilters = map[string]bool{
"label": true,
"label!": true,
"until": true,
}
)
if values := filter.Get("dangling"); len(values) > 0 {
type Filter struct {
args filters.Args
filterByUse, danglingOnly bool
until time.Time
// IDAlsoMatchesName makes the "id" filter term also match against
// network names.
IDAlsoMatchesName bool
}
type FilterNetwork interface {
Driver() string
Name() string
ID() string
Labels() map[string]string
Scope() string
Created() time.Time
HasContainerAttachments() bool
HasServiceAttachments() bool
}
// NewFilter returns a network list filter that filters by the provided args.
//
// An [errdefs.InvalidParameter] error is returned if the filter args are not
// well-formed.
func NewFilter(args filters.Args) (Filter, error) {
if err := args.Validate(acceptedFilters); err != nil {
return Filter{}, err
}
return newFilter(args)
}
// NewPruneFilter returns a network prune filter that filters by the provided args.
//
// The filter matches dangling networks which also match args.
func NewPruneFilter(args filters.Args) (Filter, error) {
if err := args.Validate(pruneFilters); err != nil {
return Filter{}, err
}
f, err := newFilter(args)
if err != nil {
return Filter{}, err
}
f.filterByUse = true
f.danglingOnly = true
return f, nil
}
func newFilter(args filters.Args) (Filter, error) {
var filterByUse, danglingOnly bool
if values := args.Get("dangling"); len(values) > 0 {
if len(values) > 1 {
return nil, errdefs.InvalidParameter(errors.New(`got more than one value for filter key "dangling"`))
return Filter{}, errdefs.InvalidParameter(errors.New(`got more than one value for filter key "dangling"`))
}
var danglingOnly bool
filterByUse = true
switch values[0] {
case "0", "false":
// dangling is false already
case "1", "true":
danglingOnly = true
default:
return nil, errdefs.InvalidParameter(errors.New(`invalid value for filter 'dangling', must be "true" (or "1"), or "false" (or "0")`))
return Filter{}, errdefs.InvalidParameter(errors.New(`invalid value for filter 'dangling', must be "true" (or "1"), or "false" (or "0")`))
}
displayNet = filterNetworkByUse(displayNet, danglingOnly)
}
if filter.Contains("type") {
typeNet := []network.Inspect{}
errFilter := filter.WalkValues("type", func(fval string) error {
passList, err := filterNetworkByType(displayNet, fval)
if err != nil {
return err
}
typeNet = append(typeNet, passList...)
return nil
})
if errFilter != nil {
return nil, errFilter
if err := args.WalkValues("type", validateNetworkTypeFilter); err != nil {
return Filter{}, err
}
until := time.Time{}
if untilFilters := args.Get("until"); len(untilFilters) > 0 {
if len(untilFilters) > 1 {
return Filter{}, errdefs.InvalidParameter(errors.New("more than one until filter specified"))
}
displayNet = typeNet
ts, err := timestamp.GetTimestamp(untilFilters[0], time.Now())
if err != nil {
return Filter{}, errdefs.InvalidParameter(err)
}
seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0)
if err != nil {
return Filter{}, errdefs.InvalidParameter(err)
}
until = time.Unix(seconds, nanoseconds)
}
return displayNet, nil
return Filter{
args: args,
filterByUse: filterByUse,
danglingOnly: danglingOnly,
until: until,
}, nil
}
func filterNetworkByUse(nws []network.Inspect, danglingOnly bool) []network.Inspect {
retNws := []network.Inspect{}
filterFunc := func(nw network.Inspect) bool {
if danglingOnly {
return !IsPredefined(nw.Name) && len(nw.Containers) == 0 && len(nw.Services) == 0
}
return IsPredefined(nw.Name) || len(nw.Containers) > 0 || len(nw.Services) > 0
}
for _, nw := range nws {
if filterFunc(nw) {
retNws = append(retNws, nw)
}
}
return retNws
func (f Filter) Get(key string) []string {
return f.args.Get(key)
}
func filterNetworkByType(nws []network.Inspect, netType string) ([]network.Inspect, error) {
retNws := []network.Inspect{}
// Matches returns true if nw satisfies the filter criteria.
func (f Filter) Matches(nw FilterNetwork) bool {
if f.args.Contains("driver") &&
!f.args.ExactMatch("driver", nw.Driver()) {
return false
}
if f.args.Contains("name") &&
!f.args.Match("name", nw.Name()) {
return false
}
if f.args.Contains("id") &&
!f.args.Match("id", nw.ID()) &&
(!f.IDAlsoMatchesName || !f.args.Match("id", nw.Name())) {
return false
}
if f.args.Contains("label") &&
!f.args.MatchKVList("label", nw.Labels()) {
return false
}
if f.args.Contains("label!") &&
f.args.MatchKVList("label!", nw.Labels()) {
return false
}
if f.args.Contains("scope") &&
!f.args.ExactMatch("scope", nw.Scope()) {
return false
}
if f.filterByUse &&
!matchesUse(f.danglingOnly, nw) {
return false
}
if netTypes := f.args.Get("type"); len(netTypes) > 0 &&
!matchesType(netTypes, nw) {
return false
}
if !f.until.IsZero() &&
nw.Created().After(f.until) {
return false
}
return true
}
func matchesUse(danglingOnly bool, nw FilterNetwork) bool {
if danglingOnly {
return !IsPredefined(nw.Name()) && !nw.HasContainerAttachments() && !nw.HasServiceAttachments()
}
return IsPredefined(nw.Name()) || nw.HasContainerAttachments() || nw.HasServiceAttachments()
}
func validateNetworkTypeFilter(netType string) error {
switch netType {
case "builtin":
for _, nw := range nws {
if IsPredefined(nw.Name) {
retNws = append(retNws, nw)
}
}
case "custom":
for _, nw := range nws {
if !IsPredefined(nw.Name) {
retNws = append(retNws, nw)
}
}
case "builtin", "custom":
return nil
default:
return nil, errors.Errorf("invalid filter: 'type'='%s'", netType)
return errors.Errorf("invalid filter: 'type'='%s'", netType)
}
return retNws, nil
}
func matchesType(netTypes []string, nw FilterNetwork) bool {
for _, netType := range netTypes {
switch netType {
case "builtin":
return IsPredefined(nw.Name())
case "custom":
return !IsPredefined(nw.Name())
}
}
return false
}

View File

@@ -3,170 +3,372 @@
package network
import (
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
)
func TestFilterNetworks(t *testing.T) {
networks := []network.Inspect{
var _ FilterNetwork = mockFilterNetwork{}
type mockFilterNetwork struct {
driver string
name string
id string
labels map[string]string
scope string
created time.Time
containers bool
services bool
}
func (n mockFilterNetwork) Driver() string {
return n.driver
}
func (n mockFilterNetwork) Name() string {
return n.name
}
func (n mockFilterNetwork) ID() string {
return n.id
}
func (n mockFilterNetwork) Labels() map[string]string {
return n.labels
}
func (n mockFilterNetwork) Scope() string {
return n.scope
}
func (n mockFilterNetwork) Created() time.Time {
return n.created
}
func (n mockFilterNetwork) HasContainerAttachments() bool {
return n.containers
}
func (n mockFilterNetwork) HasServiceAttachments() bool {
return n.services
}
func TestFilter(t *testing.T) {
networks := []mockFilterNetwork{
{
Name: "host",
Driver: "host",
Scope: "local",
name: network.NetworkHost,
id: "ubfg", // ROT13(name)
driver: "host",
scope: "local",
created: time.Date(2025, time.June, 1, 0, 0, 0, 0, time.Local),
},
{
Name: "bridge",
Driver: "bridge",
Scope: "local",
name: network.NetworkBridge,
id: "oevqtr",
driver: "bridge",
scope: "local",
created: time.Date(2025, time.June, 1, 0, 0, 0, 0, time.Local),
},
{
Name: "none",
Driver: "null",
Scope: "local",
name: network.NetworkNone,
id: "abar",
driver: "null",
scope: "local",
created: time.Date(2025, time.June, 1, 0, 0, 0, 0, time.Local),
},
{
Name: "myoverlay",
Driver: "overlay",
Scope: "swarm",
name: "myoverlay",
id: "zlbireynl",
driver: "overlay",
scope: "swarm",
created: time.Date(2024, time.June, 1, 12, 0, 0, 0, time.Local),
},
{
Name: "mydrivernet",
Driver: "mydriver",
Scope: "local",
name: "mydrivernet",
id: "zlqevirearg",
driver: "mydriver",
scope: "local",
labels: map[string]string{"foo": "bar"},
created: time.Date(2024, time.December, 1, 2, 0, 0, 0, time.Local),
},
{
Name: "mykvnet",
Driver: "mykvdriver",
Scope: "global",
name: "mykvnet",
id: "zlxiarg",
driver: "mykvdriver",
scope: "global",
created: time.Date(2025, time.January, 1, 0, 0, 0, 0, time.Local),
},
{
Name: "networkwithcontainer",
Driver: "nwc",
Scope: "local",
Containers: map[string]network.EndpointResource{
"customcontainer": {
Name: "customendpoint",
},
},
name: "networkwithcontainer",
id: "argjbexjvgupbagnvare",
driver: "nwc",
scope: "local",
containers: true,
created: time.Date(2025, time.June, 1, 0, 0, 0, 0, time.Local),
},
{
name: "networkwithservice",
id: "argjbexjvgufreivpr",
driver: "nwc",
scope: "local",
services: true,
created: time.Date(2025, time.June, 1, 0, 0, 0, 0, time.Local),
},
{
name: "idoverlap",
id: "aaaaa0my0bbbbbb",
driver: "nwc",
scope: "local",
created: time.Date(2025, time.February, 1, 0, 0, 0, 0, time.Local),
},
}
testCases := []struct {
filter filters.Args
resultCount int
err string
name string
results []string
subtest string
filter filters.Args
pruneFilter bool
idAlsoMatchesName bool
err string
results []string
}{
{
filter: filters.NewArgs(filters.Arg("driver", "bridge")),
resultCount: 1,
err: "",
name: "bridge driver filters",
subtest: "EmptyFilter",
filter: filters.NewArgs(),
results: (func() []string {
var r []string
for _, n := range networks {
r = append(r, n.name)
}
return r
})(),
},
{
filter: filters.NewArgs(filters.Arg("driver", "overlay")),
resultCount: 1,
err: "",
name: "overlay driver filters",
subtest: "driver=bridge",
filter: filters.NewArgs(filters.Arg("driver", "bridge")),
results: []string{"bridge"},
},
{
filter: filters.NewArgs(filters.Arg("driver", "noname")),
resultCount: 0,
err: "",
name: "no name driver filters",
subtest: "driver=overlay",
filter: filters.NewArgs(filters.Arg("driver", "overlay")),
results: []string{"myoverlay"},
},
{
filter: filters.NewArgs(filters.Arg("type", "custom")),
resultCount: 4,
err: "",
name: "custom driver filters",
subtest: "driver=noname",
filter: filters.NewArgs(filters.Arg("driver", "noname")),
},
{
filter: filters.NewArgs(filters.Arg("type", "builtin")),
resultCount: 3,
err: "",
name: "builtin driver filters",
subtest: "type=custom",
filter: filters.NewArgs(filters.Arg("type", "custom")),
results: []string{"myoverlay", "mydrivernet", "mykvnet", "networkwithcontainer", "networkwithservice", "idoverlap"},
},
{
filter: filters.NewArgs(filters.Arg("type", "invalid")),
resultCount: 0,
err: "invalid filter: 'type'='invalid'",
name: "invalid driver filters",
subtest: "type=builtin",
filter: filters.NewArgs(filters.Arg("type", "builtin")),
results: []string{network.NetworkHost, network.NetworkBridge, network.NetworkNone},
},
{
filter: filters.NewArgs(filters.Arg("scope", "local")),
resultCount: 5,
err: "",
name: "local scope filters",
subtest: "type=invalid",
filter: filters.NewArgs(filters.Arg("type", "invalid")),
err: "invalid filter: 'type'='invalid'",
},
{
filter: filters.NewArgs(filters.Arg("scope", "swarm")),
resultCount: 1,
err: "",
name: "swarm scope filters",
subtest: "scope=local",
filter: filters.NewArgs(filters.Arg("scope", "local")),
results: []string{network.NetworkHost, network.NetworkBridge, network.NetworkNone, "mydrivernet", "networkwithcontainer", "networkwithservice", "idoverlap"},
},
{
filter: filters.NewArgs(filters.Arg("scope", "global")),
resultCount: 1,
err: "",
name: "global scope filters",
subtest: "scope=swarm",
filter: filters.NewArgs(filters.Arg("scope", "swarm")),
results: []string{"myoverlay"},
},
{
filter: filters.NewArgs(filters.Arg("dangling", "true")),
resultCount: 3,
err: "",
name: "dangling filter is 'True'",
subtest: "scope=global",
filter: filters.NewArgs(filters.Arg("scope", "global")),
results: []string{"mykvnet"},
},
{
subtest: "dangling=true",
filter: filters.NewArgs(filters.Arg("dangling", "true")),
results: []string{"myoverlay", "mydrivernet", "mykvnet", "idoverlap"},
},
{
subtest: "dangling=1",
filter: filters.NewArgs(filters.Arg("dangling", "1")),
results: []string{"myoverlay", "mydrivernet", "mykvnet", "idoverlap"},
},
{
subtest: "dangling=false",
filter: filters.NewArgs(filters.Arg("dangling", "false")),
results: []string{network.NetworkHost, network.NetworkBridge, network.NetworkNone, "networkwithcontainer", "networkwithservice"},
},
{
subtest: "dangling=0",
filter: filters.NewArgs(filters.Arg("dangling", "0")),
results: []string{network.NetworkHost, network.NetworkBridge, network.NetworkNone, "networkwithcontainer", "networkwithservice"},
},
{
subtest: "dangling=asdf",
filter: filters.NewArgs(filters.Arg("dangling", "asdf")),
err: "invalid value for filter 'dangling'",
},
{
subtest: "MultipleTerms=dangling",
filter: filters.NewArgs(filters.Arg("dangling", "1"), filters.Arg("dangling", "true")),
err: `got more than one value for filter key "dangling"`,
},
{
subtest: "label=foo",
filter: filters.NewArgs(filters.Arg("label", "foo")),
results: []string{"mydrivernet"},
},
{
subtest: "label=foo=bar",
filter: filters.NewArgs(filters.Arg("label", "foo=bar")),
results: []string{"mydrivernet"},
},
{
subtest: "label=foo=baz",
filter: filters.NewArgs(filters.Arg("label", "foo=baz")),
},
{
subtest: "label!=foo",
filter: filters.NewArgs(filters.Arg("label!", "foo")),
err: "invalid filter 'label!'",
},
{
subtest: "until=1h",
filter: filters.NewArgs(filters.Arg("until", "1h")),
err: "invalid filter 'until'",
},
{
subtest: "name=with",
filter: filters.NewArgs(filters.Arg("name", "with")),
results: []string{"networkwithcontainer", "networkwithservice"},
},
{
subtest: "id=with/IDAlsoMatchesName=False",
filter: filters.NewArgs(filters.Arg("id", "with")),
},
{
subtest: "id=with/IDAlsoMatchesName=True",
filter: filters.NewArgs(filters.Arg("id", "with")),
idAlsoMatchesName: true,
results: []string{"networkwithcontainer", "networkwithservice"},
},
{
subtest: "id=jbexjvgu/IDAlsoMatchesName=False",
filter: filters.NewArgs(filters.Arg("id", "argjbex")),
results: []string{"networkwithcontainer", "networkwithservice"},
},
{
subtest: "id=jbexjvgu/IDAlsoMatchesName=True",
filter: filters.NewArgs(filters.Arg("id", "argjbex")),
idAlsoMatchesName: true,
results: []string{"networkwithcontainer", "networkwithservice"},
},
{
subtest: "id=my/IDAlsoMatchesName=False",
filter: filters.NewArgs(filters.Arg("id", "my")),
results: []string{"idoverlap"},
},
{
subtest: "id=my/IDAlsoMatchesName=True",
filter: filters.NewArgs(filters.Arg("id", "my")),
idAlsoMatchesName: true,
results: []string{"myoverlay", "mydrivernet", "mykvnet", "idoverlap"},
},
{
subtest: "Prune/empty",
filter: filters.NewArgs(),
pruneFilter: true,
// Prune filters have an implicit dangling=true
results: []string{"myoverlay", "mydrivernet", "mykvnet", "idoverlap"},
},
{
subtest: "Prune/label=foo",
filter: filters.NewArgs(filters.Arg("label", "foo")),
pruneFilter: true,
results: []string{"mydrivernet"},
},
{
subtest: "Prune/label=foo=bar",
filter: filters.NewArgs(filters.Arg("label", "foo=bar")),
pruneFilter: true,
results: []string{"mydrivernet"},
},
{
subtest: "Prune/label=foo=baz",
filter: filters.NewArgs(filters.Arg("label", "foo=baz")),
pruneFilter: true,
},
{
subtest: "Prune/label!=foo",
filter: filters.NewArgs(filters.Arg("label!", "foo")),
pruneFilter: true,
results: []string{"myoverlay", "mykvnet", "idoverlap"},
},
{
subtest: "Prune/until=2025-01-01",
filter: filters.NewArgs(filters.Arg("until", "2025-01-01")),
pruneFilter: true,
results: []string{"myoverlay", "mydrivernet", "mykvnet"},
},
{
filter: filters.NewArgs(filters.Arg("dangling", "false")),
resultCount: 4,
err: "",
name: "dangling filter is 'False'",
results: []string{"host", "bridge", "none", "networkwithcontainer"},
subtest: "Prune/until=2024-12-01T01:00:00",
filter: filters.NewArgs(filters.Arg("until", "2024-12-01T01:00:00")),
pruneFilter: true,
results: []string{"myoverlay"},
},
{
subtest: "Prune/MultipleTerms=until",
filter: filters.NewArgs(filters.Arg("until", "2024-12-01T01:00:00"), filters.Arg("until", "2h")),
pruneFilter: true,
err: "more than one until filter specified",
},
{
subtest: "Prune/id=invalid",
filter: filters.NewArgs(filters.Arg("id", "invalid")),
pruneFilter: true,
err: "invalid filter 'id'",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ls := make([]network.Inspect, 0, len(networks))
ls = append(ls, networks...)
result, err := FilterNetworks(ls, testCase.filter)
if testCase.err != "" {
if err == nil {
t.Fatalf("expect error '%s', got no error", testCase.err)
} else if !strings.Contains(err.Error(), testCase.err) {
t.Fatalf("expect error '%s', got '%s'", testCase.err, err)
}
t.Run(testCase.subtest, func(t *testing.T) {
var (
flt Filter
err error
)
if testCase.pruneFilter {
flt, err = NewPruneFilter(testCase.filter)
} else {
if err != nil {
t.Fatalf("expect no error, got error '%s'", err)
}
// Make sure result is not nil
if result == nil {
t.Fatal("filterNetworks should not return nil")
}
flt, err = NewFilter(testCase.filter)
}
if testCase.err != "" {
assert.ErrorContains(t, err, testCase.err)
return
}
assert.NilError(t, err)
if len(result) != testCase.resultCount {
t.Fatalf("expect '%d' networks, got '%d' networks", testCase.resultCount, len(result))
}
if len(testCase.results) > 0 {
resultMap := make(map[string]bool)
for _, r := range result {
resultMap[r.Name] = true
}
for _, r := range testCase.results {
if _, ok := resultMap[r]; !ok {
t.Fatalf("expected result: '%s' not found", r)
}
}
flt.IDAlsoMatchesName = testCase.idAlsoMatchesName
got := map[string]bool{}
for _, nw := range networks {
if flt.Matches(nw) {
got[nw.Name()] = true
}
}
want := map[string]bool{}
for _, r := range testCase.results {
want[r] = true
}
assert.Check(t, is.DeepEqual(got, want))
})
}
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/moby/moby/v2/daemon/internal/lazyregexp"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"github.com/moby/moby/v2/daemon/libnetwork"
dnetwork "github.com/moby/moby/v2/daemon/network"
"github.com/moby/moby/v2/daemon/server/backend"
"github.com/moby/moby/v2/errdefs"
"github.com/pkg/errors"
@@ -28,12 +29,6 @@ var (
"label!": true,
"until": true,
}
networksAcceptedFilters = map[string]bool{
"label": true,
"label!": true,
"until": true,
}
)
// ContainersPrune removes unused containers
@@ -96,11 +91,9 @@ func (daemon *Daemon) ContainersPrune(ctx context.Context, pruneFilters filters.
}
// localNetworksPrune removes unused local networks
func (daemon *Daemon) localNetworksPrune(ctx context.Context, pruneFilters filters.Args) *network.PruneReport {
func (daemon *Daemon) localNetworksPrune(ctx context.Context, pruneFilters dnetwork.Filter) *network.PruneReport {
rep := &network.PruneReport{}
until, _ := getUntilFromPruneFilters(pruneFilters)
// When the function returns true, the walk will stop.
daemon.netController.WalkNetworks(func(nw *libnetwork.Network) bool {
select {
@@ -112,10 +105,7 @@ func (daemon *Daemon) localNetworksPrune(ctx context.Context, pruneFilters filte
if nw.ConfigOnly() {
return false
}
if !until.IsZero() && nw.Created().After(until) {
return false
}
if !matchLabels(pruneFilters, nw.Labels()) {
if !pruneFilters.Matches(nw) {
return false
}
if !nw.IsPruneable() {
@@ -137,11 +127,9 @@ func (daemon *Daemon) localNetworksPrune(ctx context.Context, pruneFilters filte
var networkIsInUse = lazyregexp.New(`network ([[:alnum:]]+) is in use`)
// clusterNetworksPrune removes unused cluster networks
func (daemon *Daemon) clusterNetworksPrune(ctx context.Context, pruneFilters filters.Args) (*network.PruneReport, error) {
func (daemon *Daemon) clusterNetworksPrune(ctx context.Context, pruneFilters dnetwork.Filter) (*network.PruneReport, error) {
rep := &network.PruneReport{}
until, _ := getUntilFromPruneFilters(pruneFilters)
cluster := daemon.GetCluster()
if !cluster.IsManager() {
@@ -162,12 +150,6 @@ func (daemon *Daemon) clusterNetworksPrune(ctx context.Context, pruneFilters fil
// Routing-mesh network removal has to be explicitly invoked by user
continue
}
if !until.IsZero() && nw.Created.After(until) {
continue
}
if !matchLabels(pruneFilters, nw.Labels) {
continue
}
// https://github.com/moby/moby/issues/24186
// `docker network inspect` unfortunately displays ONLY those containers that are local to that node.
// So we try to remove it anyway and check the error
@@ -187,22 +169,17 @@ func (daemon *Daemon) clusterNetworksPrune(ctx context.Context, pruneFilters fil
}
// NetworksPrune removes unused networks
func (daemon *Daemon) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (*network.PruneReport, error) {
func (daemon *Daemon) NetworksPrune(ctx context.Context, filterArgs filters.Args) (*network.PruneReport, error) {
if !daemon.pruneRunning.CompareAndSwap(false, true) {
return nil, errPruneRunning
}
defer daemon.pruneRunning.Store(false)
// make sure that only accepted filters have been received
err := pruneFilters.Validate(networksAcceptedFilters)
pruneFilters, err := dnetwork.NewPruneFilter(filterArgs)
if err != nil {
return nil, err
}
if _, err := getUntilFromPruneFilters(pruneFilters); err != nil {
return nil, err
}
rep := &network.PruneReport{}
if clusterRep, err := daemon.clusterNetworksPrune(ctx, pruneFilters); err == nil {
rep.NetworksDeleted = append(rep.NetworksDeleted, clusterRep.NetworksDeleted...)

View File

@@ -5,13 +5,14 @@ import (
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
dnetwork "github.com/moby/moby/v2/daemon/network"
"github.com/moby/moby/v2/daemon/server/backend"
)
// Backend is all the methods that need to be implemented
// to provide network specific functionality.
type Backend interface {
GetNetworks(filters.Args, backend.NetworkListConfig) ([]network.Inspect, error)
GetNetworks(dnetwork.Filter, backend.NetworkListConfig) ([]network.Inspect, error)
CreateNetwork(ctx context.Context, nc network.CreateRequest) (*network.CreateResponse, error)
ConnectContainerToNetwork(ctx context.Context, containerName, networkName string, endpointConfig *network.EndpointSettings) error
DisconnectContainerFromNetwork(containerName string, networkName string, force bool) error
@@ -22,7 +23,7 @@ type Backend interface {
// ClusterBackend is all the methods that need to be implemented
// to provide cluster network specific functionality.
type ClusterBackend interface {
GetNetworks(filters.Args) ([]network.Inspect, error)
GetNetworks(dnetwork.Filter) ([]network.Inspect, error)
GetNetwork(name string) (network.Inspect, error)
GetNetworksByName(name string) ([]network.Inspect, error)
CreateNetwork(nc network.CreateRequest) (string, error)

View File

@@ -11,6 +11,7 @@ import (
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/v2/daemon/libnetwork"
"github.com/moby/moby/v2/daemon/libnetwork/scope"
dnetwork "github.com/moby/moby/v2/daemon/network"
"github.com/moby/moby/v2/daemon/server/backend"
"github.com/moby/moby/v2/daemon/server/httputils"
"github.com/moby/moby/v2/daemon/server/networkbackend"
@@ -23,12 +24,13 @@ func (n *networkRouter) getNetworksList(ctx context.Context, w http.ResponseWrit
return err
}
filter, err := filters.FromJSON(r.Form.Get("filters"))
filterArgs, err := filters.FromJSON(r.Form.Get("filters"))
if err != nil {
return err
}
if err := network.ValidateFilters(filter); err != nil {
filter, err := dnetwork.NewFilter(filterArgs)
if err != nil {
return err
}
@@ -114,10 +116,16 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
// TODO(@cpuguy83): All this logic for figuring out which network to return does not belong here
// Instead there should be a backend function to just get one network.
filter := filters.NewArgs(filters.Arg("idOrName", term))
filterArgs := filters.NewArgs(filters.Arg("id", term))
if networkScope != "" {
filter.Add("scope", networkScope)
filterArgs.Add("scope", networkScope)
}
filter, err := dnetwork.NewFilter(filterArgs)
if err != nil {
return err
}
filter.IDAlsoMatchesName = true
networks, _ := n.backend.GetNetworks(filter, backend.NetworkListConfig{Detailed: true, Verbose: verbose})
for _, nw := range networks {
if nw.ID == term {
@@ -322,7 +330,12 @@ func (n *networkRouter) findUniqueNetwork(term string) (network.Inspect, error)
listByFullName := map[string]network.Inspect{}
listByPartialID := map[string]network.Inspect{}
filter := filters.NewArgs(filters.Arg("idOrName", term))
filter, err := dnetwork.NewFilter(filters.NewArgs(filters.Arg("id", term)))
if err != nil {
return network.Inspect{}, err
}
filter.IDAlsoMatchesName = true
networks, _ := n.backend.GetNetworks(filter, backend.NetworkListConfig{Detailed: true})
for _, nw := range networks {
if nw.ID == term {

View File

@@ -2,8 +2,6 @@ package network
import (
"time"
"github.com/moby/moby/api/types/filters"
)
const (
@@ -116,21 +114,6 @@ type ConfigReference struct {
Network string
}
var acceptedFilters = map[string]bool{
"dangling": true,
"driver": true,
"id": true,
"label": true,
"name": true,
"scope": true,
"type": true,
}
// ValidateFilters validates the list of filter args with the available filters.
func ValidateFilters(filter filters.Args) error {
return filter.Validate(acceptedFilters)
}
// PruneReport contains the response for Engine API:
// POST "/networks/prune"
type PruneReport struct {