This fix tries to add a `scope` in the query of `/networks/<id>`
(`NetworkInspect`) so that in case of duplicate network names,
it is possible to locate the network ID based on the network
scope (`local`, 'swarm', or `global`).
Multiple networks might exist in different scopes, which is a legitimate case.
For example, a network name `foo` might exists locally and in swarm network.
However, before this PR it was not possible to query a network name `foo`
in a specific scope like swarm.
This fix fixes the issue by allowing a `scope` query in `/networks/<id>`.
Additional test cases have been added to unit tests and integration tests.
This fix is related to docker/cli#167, moby/moby#30897, moby/moby#33561, moby/moby#30242
This fix fixes docker/cli#167
Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
| ... | ... |
@@ -98,6 +98,14 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r |
| 98 | 98 |
return errors.NewBadRequestError(err) |
| 99 | 99 |
} |
| 100 | 100 |
} |
| 101 |
+ scope := r.URL.Query().Get("scope")
|
|
| 102 |
+ |
|
| 103 |
+ isMatchingScope := func(scope, term string) bool {
|
|
| 104 |
+ if term != "" {
|
|
| 105 |
+ return scope == term |
|
| 106 |
+ } |
|
| 107 |
+ return true |
|
| 108 |
+ } |
|
| 101 | 109 |
|
| 102 | 110 |
// In case multiple networks have duplicate names, return error. |
| 103 | 111 |
// TODO (yongtang): should we wrap with version here for backward compatibility? |
| ... | ... |
@@ -112,15 +120,15 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r |
| 112 | 112 |
|
| 113 | 113 |
nw := n.backend.GetNetworks() |
| 114 | 114 |
for _, network := range nw {
|
| 115 |
- if network.ID() == term {
|
|
| 115 |
+ if network.ID() == term && isMatchingScope(network.Info().Scope(), scope) {
|
|
| 116 | 116 |
return httputils.WriteJSON(w, http.StatusOK, *n.buildDetailedNetworkResources(network, verbose)) |
| 117 | 117 |
} |
| 118 |
- if network.Name() == term {
|
|
| 118 |
+ if network.Name() == term && isMatchingScope(network.Info().Scope(), scope) {
|
|
| 119 | 119 |
// No need to check the ID collision here as we are still in |
| 120 | 120 |
// local scope and the network ID is unique in this scope. |
| 121 | 121 |
listByFullName[network.ID()] = *n.buildDetailedNetworkResources(network, verbose) |
| 122 | 122 |
} |
| 123 |
- if strings.HasPrefix(network.ID(), term) {
|
|
| 123 |
+ if strings.HasPrefix(network.ID(), term) && isMatchingScope(network.Info().Scope(), scope) {
|
|
| 124 | 124 |
// No need to check the ID collision here as we are still in |
| 125 | 125 |
// local scope and the network ID is unique in this scope. |
| 126 | 126 |
listByPartialID[network.ID()] = *n.buildDetailedNetworkResources(network, verbose) |
| ... | ... |
@@ -129,10 +137,10 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r |
| 129 | 129 |
|
| 130 | 130 |
nr, _ := n.cluster.GetNetworks() |
| 131 | 131 |
for _, network := range nr {
|
| 132 |
- if network.ID == term {
|
|
| 132 |
+ if network.ID == term && isMatchingScope(network.Scope, scope) {
|
|
| 133 | 133 |
return httputils.WriteJSON(w, http.StatusOK, network) |
| 134 | 134 |
} |
| 135 |
- if network.Name == term {
|
|
| 135 |
+ if network.Name == term && isMatchingScope(network.Scope, scope) {
|
|
| 136 | 136 |
// Check the ID collision as we are in swarm scope here, and |
| 137 | 137 |
// the map (of the listByFullName) may have already had a |
| 138 | 138 |
// network with the same ID (from local scope previously) |
| ... | ... |
@@ -140,7 +148,7 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r |
| 140 | 140 |
listByFullName[network.ID] = network |
| 141 | 141 |
} |
| 142 | 142 |
} |
| 143 |
- if strings.HasPrefix(network.ID, term) {
|
|
| 143 |
+ if strings.HasPrefix(network.ID, term) && isMatchingScope(network.Scope, scope) {
|
|
| 144 | 144 |
// Check the ID collision as we are in swarm scope here, and |
| 145 | 145 |
// the map (of the listByPartialID) may have already had a |
| 146 | 146 |
// network with the same ID (from local scope previously) |
| ... | ... |
@@ -6437,6 +6437,10 @@ paths: |
| 6437 | 6437 |
description: "Detailed inspect output for troubleshooting" |
| 6438 | 6438 |
type: "boolean" |
| 6439 | 6439 |
default: false |
| 6440 |
+ - name: "scope" |
|
| 6441 |
+ in: "query" |
|
| 6442 |
+ description: "Filter the network by scope (swarm, global, or local)" |
|
| 6443 |
+ type: "string" |
|
| 6440 | 6444 |
tags: ["Network"] |
| 6441 | 6445 |
|
| 6442 | 6446 |
delete: |
| ... | ... |
@@ -468,6 +468,12 @@ type NetworkDisconnect struct {
|
| 468 | 468 |
Force bool |
| 469 | 469 |
} |
| 470 | 470 |
|
| 471 |
+// NetworkInspectOptions holds parameters to inspect network |
|
| 472 |
+type NetworkInspectOptions struct {
|
|
| 473 |
+ Scope string |
|
| 474 |
+ Verbose bool |
|
| 475 |
+} |
|
| 476 |
+ |
|
| 471 | 477 |
// Checkpoint represents the details of a checkpoint |
| 472 | 478 |
type Checkpoint struct {
|
| 473 | 479 |
Name string // Name is the name of the checkpoint |
| ... | ... |
@@ -99,8 +99,8 @@ type NetworkAPIClient interface {
|
| 99 | 99 |
NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error |
| 100 | 100 |
NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) |
| 101 | 101 |
NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error |
| 102 |
- NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) |
|
| 103 |
- NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error) |
|
| 102 |
+ NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) |
|
| 103 |
+ NetworkInspectWithRaw(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error) |
|
| 104 | 104 |
NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) |
| 105 | 105 |
NetworkRemove(ctx context.Context, networkID string) error |
| 106 | 106 |
NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) |
| ... | ... |
@@ -12,22 +12,25 @@ import ( |
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 | 14 |
// NetworkInspect returns the information for a specific network configured in the docker host. |
| 15 |
-func (cli *Client) NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) {
|
|
| 16 |
- networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID, verbose) |
|
| 15 |
+func (cli *Client) NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
|
| 16 |
+ networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID, options) |
|
| 17 | 17 |
return networkResource, err |
| 18 | 18 |
} |
| 19 | 19 |
|
| 20 | 20 |
// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation. |
| 21 |
-func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error) {
|
|
| 21 |
+func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
|
|
| 22 | 22 |
var ( |
| 23 | 23 |
networkResource types.NetworkResource |
| 24 | 24 |
resp serverResponse |
| 25 | 25 |
err error |
| 26 | 26 |
) |
| 27 | 27 |
query := url.Values{}
|
| 28 |
- if verbose {
|
|
| 28 |
+ if options.Verbose {
|
|
| 29 | 29 |
query.Set("verbose", "true")
|
| 30 | 30 |
} |
| 31 |
+ if options.Scope != "" {
|
|
| 32 |
+ query.Set("scope", options.Scope)
|
|
| 33 |
+ } |
|
| 31 | 34 |
resp, err = cli.get(ctx, "/networks/"+networkID, query, nil) |
| 32 | 35 |
if err != nil {
|
| 33 | 36 |
if resp.statusCode == http.StatusNotFound {
|
| ... | ... |
@@ -11,6 +11,7 @@ import ( |
| 11 | 11 |
|
| 12 | 12 |
"github.com/docker/docker/api/types" |
| 13 | 13 |
"github.com/docker/docker/api/types/network" |
| 14 |
+ "github.com/stretchr/testify/assert" |
|
| 14 | 15 |
"golang.org/x/net/context" |
| 15 | 16 |
) |
| 16 | 17 |
|
| ... | ... |
@@ -19,7 +20,7 @@ func TestNetworkInspectError(t *testing.T) {
|
| 19 | 19 |
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
| 20 | 20 |
} |
| 21 | 21 |
|
| 22 |
- _, err := client.NetworkInspect(context.Background(), "nothing", false) |
|
| 22 |
+ _, err := client.NetworkInspect(context.Background(), "nothing", types.NetworkInspectOptions{})
|
|
| 23 | 23 |
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
| 24 | 24 |
t.Fatalf("expected a Server Error, got %v", err)
|
| 25 | 25 |
} |
| ... | ... |
@@ -30,7 +31,7 @@ func TestNetworkInspectContainerNotFound(t *testing.T) {
|
| 30 | 30 |
client: newMockClient(errorMock(http.StatusNotFound, "Server error")), |
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 |
- _, err := client.NetworkInspect(context.Background(), "unknown", false) |
|
| 33 |
+ _, err := client.NetworkInspect(context.Background(), "unknown", types.NetworkInspectOptions{})
|
|
| 34 | 34 |
if err == nil || !IsErrNetworkNotFound(err) {
|
| 35 | 35 |
t.Fatalf("expected a networkNotFound error, got %v", err)
|
| 36 | 36 |
} |
| ... | ... |
@@ -51,7 +52,14 @@ func TestNetworkInspect(t *testing.T) {
|
| 51 | 51 |
content []byte |
| 52 | 52 |
err error |
| 53 | 53 |
) |
| 54 |
- if strings.HasPrefix(req.URL.RawQuery, "verbose=true") {
|
|
| 54 |
+ if strings.Contains(req.URL.RawQuery, "scope=global") {
|
|
| 55 |
+ return &http.Response{
|
|
| 56 |
+ StatusCode: http.StatusNotFound, |
|
| 57 |
+ Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 58 |
+ }, nil |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ if strings.Contains(req.URL.RawQuery, "verbose=true") {
|
|
| 55 | 62 |
s := map[string]network.ServiceInfo{
|
| 56 | 63 |
"web": {},
|
| 57 | 64 |
} |
| ... | ... |
@@ -74,7 +82,7 @@ func TestNetworkInspect(t *testing.T) {
|
| 74 | 74 |
}), |
| 75 | 75 |
} |
| 76 | 76 |
|
| 77 |
- r, err := client.NetworkInspect(context.Background(), "network_id", false) |
|
| 77 |
+ r, err := client.NetworkInspect(context.Background(), "network_id", types.NetworkInspectOptions{})
|
|
| 78 | 78 |
if err != nil {
|
| 79 | 79 |
t.Fatal(err) |
| 80 | 80 |
} |
| ... | ... |
@@ -82,7 +90,7 @@ func TestNetworkInspect(t *testing.T) {
|
| 82 | 82 |
t.Fatalf("expected `mynetwork`, got %s", r.Name)
|
| 83 | 83 |
} |
| 84 | 84 |
|
| 85 |
- r, err = client.NetworkInspect(context.Background(), "network_id", true) |
|
| 85 |
+ r, err = client.NetworkInspect(context.Background(), "network_id", types.NetworkInspectOptions{Verbose: true})
|
|
| 86 | 86 |
if err != nil {
|
| 87 | 87 |
t.Fatal(err) |
| 88 | 88 |
} |
| ... | ... |
@@ -93,4 +101,7 @@ func TestNetworkInspect(t *testing.T) {
|
| 93 | 93 |
if !ok {
|
| 94 | 94 |
t.Fatalf("expected service `web` missing in the verbose output")
|
| 95 | 95 |
} |
| 96 |
+ |
|
| 97 |
+ _, err = client.NetworkInspect(context.Background(), "network_id", types.NetworkInspectOptions{Scope: "global"})
|
|
| 98 |
+ assert.EqualError(t, err, "Error: No such network: network_id") |
|
| 96 | 99 |
} |
| ... | ... |
@@ -21,6 +21,7 @@ keywords: "API, Docker, rcli, REST, documentation" |
| 21 | 21 |
* `POST /secrets/create` now returns status code 409 instead of 500 when creating an already existing secret. |
| 22 | 22 |
* `POST /secrets/(name)/update` now returns status code 400 instead of 500 when updating a secret's content which is not the labels. |
| 23 | 23 |
* `POST /nodes/(name)/update` now returns status code 400 instead of 500 when demoting last node fails. |
| 24 |
+* `GET /networks/(id or name)` now takes an optional query parameter `scope` that will filter the network based on the scope (`local`, `swarm`, or `global`). |
|
| 24 | 25 |
|
| 25 | 26 |
## v1.30 API changes |
| 26 | 27 |
|
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"io/ioutil" |
| 9 | 9 |
"net" |
| 10 | 10 |
"net/http" |
| 11 |
+ "net/url" |
|
| 11 | 12 |
"os" |
| 12 | 13 |
"path/filepath" |
| 13 | 14 |
"strings" |
| ... | ... |
@@ -1002,3 +1003,36 @@ func (s *DockerSwarmSuite) TestSwarmRepeatedRootRotation(c *check.C) {
|
| 1002 | 1002 |
currentTrustRoot = clusterTLSInfo.TrustRoot |
| 1003 | 1003 |
} |
| 1004 | 1004 |
} |
| 1005 |
+ |
|
| 1006 |
+func (s *DockerSwarmSuite) TestAPINetworkInspectWithScope(c *check.C) {
|
|
| 1007 |
+ d := s.AddDaemon(c, true, true) |
|
| 1008 |
+ |
|
| 1009 |
+ name := "foo" |
|
| 1010 |
+ networkCreateRequest := types.NetworkCreateRequest{
|
|
| 1011 |
+ Name: name, |
|
| 1012 |
+ } |
|
| 1013 |
+ |
|
| 1014 |
+ var n types.NetworkCreateResponse |
|
| 1015 |
+ networkCreateRequest.NetworkCreate.Driver = "overlay" |
|
| 1016 |
+ |
|
| 1017 |
+ status, out, err := d.SockRequest("POST", "/networks/create", networkCreateRequest)
|
|
| 1018 |
+ c.Assert(err, checker.IsNil, check.Commentf(string(out))) |
|
| 1019 |
+ c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(out))) |
|
| 1020 |
+ c.Assert(json.Unmarshal(out, &n), checker.IsNil) |
|
| 1021 |
+ |
|
| 1022 |
+ var r types.NetworkResource |
|
| 1023 |
+ |
|
| 1024 |
+ status, body, err := d.SockRequest("GET", "/networks/"+name, nil)
|
|
| 1025 |
+ c.Assert(err, checker.IsNil, check.Commentf(string(out))) |
|
| 1026 |
+ c.Assert(status, checker.Equals, http.StatusOK, check.Commentf(string(out))) |
|
| 1027 |
+ c.Assert(json.Unmarshal(body, &r), checker.IsNil) |
|
| 1028 |
+ c.Assert(r.Scope, checker.Equals, "swarm") |
|
| 1029 |
+ c.Assert(r.ID, checker.Equals, n.ID) |
|
| 1030 |
+ |
|
| 1031 |
+ v := url.Values{}
|
|
| 1032 |
+ v.Set("scope", "local")
|
|
| 1033 |
+ |
|
| 1034 |
+ status, body, err = d.SockRequest("GET", "/networks/"+name+"?"+v.Encode(), nil)
|
|
| 1035 |
+ c.Assert(err, checker.IsNil, check.Commentf(string(out))) |
|
| 1036 |
+ c.Assert(status, checker.Equals, http.StatusNotFound, check.Commentf(string(out))) |
|
| 1037 |
+} |