diff --git a/apis/server/network_bridge.go b/apis/server/network_bridge.go index d33292cb71..79eed225f6 100644 --- a/apis/server/network_bridge.go +++ b/apis/server/network_bridge.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/alibaba/pouch/apis/types" + "github.com/alibaba/pouch/daemon/mgr" networktypes "github.com/alibaba/pouch/network/types" "github.com/alibaba/pouch/pkg/httputils" @@ -112,6 +113,46 @@ func (s *Server) disconnectNetwork(ctx context.Context, rw http.ResponseWriter, return s.ContainerMgr.Disconnect(ctx, network.Container, id, network.Force) } +func (s *Server) pruneNetwork(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { + var volumeResult []string + + networkLists, err := s.NetworkMgr.List(ctx, map[string]string{}) + if err != nil { + return err + } + + toDeleteFlag := map[string]bool{} + toDeleteFlag["none"] = true + toDeleteFlag["host"] = true + toDeleteFlag["bridge"] = true + + _, err = s.ContainerMgr.List(ctx, &mgr.ContainerListOption{ + All: true, + FilterFunc: func(c *mgr.Container) bool { + if len(c.NetworkSettings.Networks) == 0 { + return false + } + for k := range c.NetworkSettings.Networks { + toDeleteFlag[k] = true + } + return true + }, + }) + + if err != nil { + return err + } + + for _, network := range networkLists { + if toDeleteFlag[network.Name] == false { + volumeResult = append(volumeResult, network.Name) + s.NetworkMgr.Remove(ctx, network.Name) + } + } + + return EncodeResponse(rw, http.StatusOK, volumeResult) +} + func buildNetworkInspectResp(n *networktypes.Network) *types.NetworkInspectResp { info := n.Network.Info() network := &types.NetworkInspectResp{ diff --git a/apis/server/router.go b/apis/server/router.go index cfb75aaa71..2953f208e9 100644 --- a/apis/server/router.go +++ b/apis/server/router.go @@ -89,6 +89,7 @@ func initRoute(s *Server) *mux.Router { {Method: http.MethodDelete, Path: "/networks/{id:.*}", HandlerFunc: s.deleteNetwork}, {Method: http.MethodPost, Path: "/networks/{id:.*}/connect", HandlerFunc: s.connectToNetwork}, {Method: http.MethodPost, Path: "/networks/{id:.*}/disconnect", HandlerFunc: s.disconnectNetwork}, + {Method: http.MethodPost, Path: "/networks/prune", HandlerFunc: s.pruneNetwork}, // metrics {Method: http.MethodGet, Path: "/metrics", HandlerFunc: s.metrics}, diff --git a/apis/swagger.yml b/apis/swagger.yml index 1088b2c283..784861c723 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -1498,6 +1498,30 @@ paths: $ref: "#/definitions/NetworkDisconnect" tags: ["Network"] + /networks/prune: + post: + summary: "Delete unused networks" + produces: + - "application/json" + operationId: "NetworkPrune" + responses: + 200: + description: "No error" + schema: + type: "object" + title: "NetworkPruneResp" + properties: + NetworksDeleted: + description: "Networks that were deleted" + type: "array" + items: + type: "string" + 500: + description: "Server error" + schema: + $ref: "#/responses/500ErrorResponse" + tags: ["Network"] + /commit: post: summary: "Create an image from a container" diff --git a/cli/network.go b/cli/network.go index c5405a4a7d..2d16b81f1e 100644 --- a/cli/network.go +++ b/cli/network.go @@ -43,6 +43,7 @@ func (n *NetworkCommand) Init(c *Cli) { c.AddCommand(n, &NetworkListCommand{}) c.AddCommand(n, &NetworkConnectCommand{}) c.AddCommand(n, &NetworkDisconnectCommand{}) + c.AddCommand(n, &NetworkPruneCommand{}) } // networkCreateDescription is used to describe network create command in detail and auto generate command doc. @@ -524,3 +525,78 @@ func (nd *NetworkDisconnectCommand) networkDisconnectExample() string { return `$ pouch network disconnect bridge test container test is disconnected from network bridge successfully` } + +// networkPruneDescription is used to describe network prune command in detail and auto generate comand doc. +var NetworkPruneDescription = "Delete all unused networks" + +// NetworkPruneCommand use to implement 'network prune' command +type NetworkPruneCommand struct { + baseCommand + force bool +} + +// Init initializes 'network prune' command. +func (n *NetworkPruneCommand) Init(c *Cli) { + n.cli = c + n.cmd = &cobra.Command{ + Use: "prune", + Short: "Prune networks", + Args: cobra.NoArgs, + Long: networkDisconnectDescription, + RunE: func(cmd *cobra.Command, args []string) error { + return n.runNetworkPrune(args) + }, + Example: n.networkPruneExample(), + } + n.addFlags() +} + +// addFlags adds flags for specific command. +func (n *NetworkPruneCommand) addFlags() { + // add flags + flagSet := n.cmd.Flags() + flagSet.BoolVarP(&n.force, "force", "f", false, "Do not prompt for confirmation") +} + +// runNetworkPrune is use to delete unused networks. +func (n *NetworkPruneCommand) runNetworkPrune(args []string) error { + logrus.Debugf("prune the network") + + ctx := context.Background() + apiClient := n.cli.Client() + + if !n.force { + fmt.Println("WARNING! This will remove all networks not used by at least one container.") + fmt.Print("Are you sure you want to continue? [y/N]") + var input string + fmt.Scanf("%s", &input) + if input != "Y" && input != "y" { + return nil + } + } + + networkResult, err := apiClient.NetworkPrune(ctx) + if err != nil { + return err + } + + if len(networkResult) > 0 { + fmt.Println("Deleted Networks:") + for _, networkName := range networkResult { + fmt.Println(networkName) + } + } + + return nil +} + +// runNetworkPrune is use to delete unused networks. +func (n *NetworkPruneCommand) networkPruneExample() string { + return `$ pouch network prune +WARNING! This will remove all networks not used by at least one container. +Are you sure you want to continue? [y/N] +Deleted Networks: +n1 +n2 +` +} diff --git a/client/interface.go b/client/interface.go index bb933ef019..615697877a 100644 --- a/client/interface.go +++ b/client/interface.go @@ -91,4 +91,5 @@ type NetworkAPIClient interface { NetworkList(ctx context.Context) ([]types.NetworkResource, error) NetworkConnect(ctx context.Context, network string, req *types.NetworkConnect) error NetworkDisconnect(ctx context.Context, networkID, containerID string, force bool) error + NetworkPrune(ctx context.Context) ([]string, error) } diff --git a/client/network_prune.go b/client/network_prune.go new file mode 100644 index 0000000000..bb148671d8 --- /dev/null +++ b/client/network_prune.go @@ -0,0 +1,20 @@ +package client + +import ( + "context" + "net/url" +) + +// NetworkPrune delete unused networks. +func (client *APIClient) NetworkPrune(ctx context.Context) ([]string, error) { + resp, err := client.post(ctx, "/networks/prune", url.Values{}, nil, nil) + if err != nil { + return nil, err + } + + result := []string{} + err = decodeBody(&result, resp.Body) + ensureCloseReader(resp) + + return result, err +} diff --git a/client/network_prune_test.go b/client/network_prune_test.go new file mode 100644 index 0000000000..f609c0a3af --- /dev/null +++ b/client/network_prune_test.go @@ -0,0 +1,66 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNetworkPruneError(t *testing.T) { + client := &APIClient{ + HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")), + } + + _, err := client.NetworkPrune(context.Background()) + + if err == nil || err.Error() == "Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworkPruneList(t *testing.T) { + expectedURL := "/networks/prune" + + httpClient := newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL) + } + + networksPruneJSON := []string{ + "network0", + "network1", + "network2", + } + + b, err := json.Marshal(networksPruneJSON) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(b))), + }, nil + }) + + client := &APIClient{ + HTTPCli: httpClient, + } + networkPruneResp, err := client.NetworkPrune(context.Background()) + if err != nil { + t.Fatal(err) + } + + if len(networkPruneResp) != 3 { + t.Fatalf("expected 3 networks, got %v", networkPruneResp) + } + assert.Equal(t, networkPruneResp[0], "network0") + assert.Equal(t, networkPruneResp[1], "network1") + assert.Equal(t, networkPruneResp[2], "network2") +} diff --git a/daemon/mgr/network.go b/daemon/mgr/network.go index 0525ee7dd3..522506eb31 100644 --- a/daemon/mgr/network.go +++ b/daemon/mgr/network.go @@ -59,6 +59,9 @@ type NetworkMgr interface { // GetNetworkStats returns the network stats of specific sandbox GetNetworkStats(sandboxID string) (map[string]apitypes.NetworkStats, error) + + // NetworkPrune is used to delete unused networks. + Prune(ctx context.Context) ([]string, error) } // NetworkManager is the default implement of interface NetworkMgr. @@ -477,6 +480,12 @@ func (nm *NetworkManager) GetNetworkStats(sandboxID string) (map[string]apitypes return stats, nil } +// Prune is used to delete unused networks. +func (nm *NetworkManager) Prune(ctx context.Context) ([]string, error) { + + return []string{"aaa", "bbb"}, nil +} + // Controller returns the network controller. func (nm *NetworkManager) Controller() libnetwork.NetworkController { return nm.controller diff --git a/test/api_network_prune_test.go b/test/api_network_prune_test.go new file mode 100644 index 0000000000..b7dc20d110 --- /dev/null +++ b/test/api_network_prune_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/alibaba/pouch/test/command" + "github.com/alibaba/pouch/test/environment" + "github.com/alibaba/pouch/test/request" + + "github.com/go-check/check" + "github.com/gotestyourself/gotestyourself/icmd" +) + +// APINetworkPruneSuite is the test suite for network prune API. +type APINetworkPruneSuite struct{} + +func init() { + check.Suite(&APINetworkPruneSuite{}) +} + +// SetUpTest does common setup in the beginning of each test. +func (suite *APINetworkPruneSuite) SetUpTest(c *check.C) { + SkipIfFalse(c, environment.IsLinux) +} + +// TestNetworkPruneOk test if prune network is ok +func (suite *APINetworkPruneSuite) TestNetworkPruneOk(c *check.C) { + network1 := "TestPruneNetwork1" + command.PouchRun("network", "create", network1, "-d", "bridge", "--gateway", "192.168.1.1", "--subnet", "192.168.1.0/24").Assert(c, icmd.Success) + + network2 := "TestPruneNetwork2" + command.PouchRun("network", "create", network2, "-d", "bridge", "--gateway", "192.168.2.1", "--subnet", "192.168.2.0/24").Assert(c, icmd.Success) + + // prune the network. + path := "/networks/prune" + resp, err := request.Post(path) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 200) + + var networkPruneResp []string + err = request.DecodeBody(&networkPruneResp, resp.Body) + c.Assert(err, check.IsNil) + c.Assert(len(networkPruneResp), check.Equals, 2) +} diff --git a/test/cli_network_test.go b/test/cli_network_test.go index 9be7699df4..cd4c6a29c0 100644 --- a/test/cli_network_test.go +++ b/test/cli_network_test.go @@ -605,3 +605,42 @@ func (suite *PouchNetworkSuite) TestNetworkConnectWithRestart(c *check.C) { c.Assert(found, check.Equals, false) } + +// TestNetworkPrune is to verify the correctness of 'network prune' command. +func (suite *PouchNetworkSuite) TestNetworkPrune(c *check.C) { + network1 := "TestPruneNetwork1" + command.PouchRun("network", "create", network1, "-d", "bridge", "--gateway", "192.168.1.1", "--subnet", "192.168.1.0/24").Assert(c, icmd.Success) + + network2 := "TestPruneNetwork2" + command.PouchRun("network", "create", network2, "-d", "bridge", "--gateway", "192.168.2.1", "--subnet", "192.168.2.0/24").Assert(c, icmd.Success) + + network3 := "TestPruneNetwork3" + command.PouchRun("network", "create", network3, "-d", "bridge", "--gateway", "192.168.3.1", "--subnet", "192.168.3.0/24").Assert(c, icmd.Success) + defer func() { + command.PouchRun("network", "rm", network3).Assert(c, icmd.Success) + }() + + // create container + containerName := "TestNetworkPruneContainer" + command.PouchRun("run", "-d", "--name", containerName, busyboxImage, "top").Assert(c, icmd.Success) + defer func() { + command.PouchRun("rm", "-f", containerName).Assert(c, icmd.Success) + }() + + // connect a network + command.PouchRun("network", "connect", network3, containerName).Assert(c, icmd.Success) + + // network prune + ret := command.PouchRun("network", "prune", "-f") + ret.Assert(c, icmd.Success) + out := ret.Combined() + + found := false + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, "network1") || strings.Contains(line, "network2") { + found = true + break + } + } + c.Assert(found, check.Equals, false) +}