diff --git a/libnetwork/ipams/remote/api/api.go b/libnetwork/ipams/remote/api/api.go index 7d60eb311d..b8d21fdc6b 100644 --- a/libnetwork/ipams/remote/api/api.go +++ b/libnetwork/ipams/remote/api/api.go @@ -2,10 +2,6 @@ // messages between libnetwork and the remote ipam plugin package api -import ( - "net" -) - // Response is the basic response structure used in all responses type Response struct { Error string @@ -41,7 +37,7 @@ type RequestPoolRequest struct { type RequestPoolResponse struct { Response PoolID string - Pool *net.IPNet + Pool string // CIDR format Data map[string]string } @@ -58,21 +54,21 @@ type ReleasePoolResponse struct { // RequestAddressRequest represents the expected data in a ``request address`` request message type RequestAddressRequest struct { PoolID string - Address net.IP + Address string Options map[string]string } // RequestAddressResponse represents the expected data in the response message to a ``request address`` request type RequestAddressResponse struct { Response - Address *net.IPNet + Address string // in CIDR format Data map[string]string } // ReleaseAddressRequest represents the expected data in a ``release address`` request message type ReleaseAddressRequest struct { PoolID string - Address net.IP + Address string } // ReleaseAddressResponse represents the response message to a ``release address`` request diff --git a/libnetwork/ipams/remote/remote.go b/libnetwork/ipams/remote/remote.go index f5ddbd4764..f9df525ced 100644 --- a/libnetwork/ipams/remote/remote.go +++ b/libnetwork/ipams/remote/remote.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/pkg/plugins" "github.com/docker/libnetwork/ipamapi" "github.com/docker/libnetwork/ipams/remote/api" + "github.com/docker/libnetwork/types" ) type allocator struct { @@ -64,7 +65,8 @@ func (a *allocator) RequestPool(addressSpace, pool, subPool string, options map[ if err := a.call("RequestPool", req, res); err != nil { return "", nil, nil, err } - return res.PoolID, res.Pool, res.Data, nil + retPool, err := types.ParseCIDR(res.Pool) + return res.PoolID, retPool, res.Data, err } // ReleasePool removes an address pool from the specified address space @@ -76,17 +78,26 @@ func (a *allocator) ReleasePool(poolID string) error { // RequestAddress requests an address from the address pool func (a *allocator) RequestAddress(poolID string, address net.IP, options map[string]string) (*net.IPNet, map[string]string, error) { - req := &api.RequestAddressRequest{PoolID: poolID, Address: address, Options: options} + var prefAddress string + if address != nil { + prefAddress = address.String() + } + req := &api.RequestAddressRequest{PoolID: poolID, Address: prefAddress, Options: options} res := &api.RequestAddressResponse{} if err := a.call("RequestAddress", req, res); err != nil { return nil, nil, err } - return res.Address, res.Data, nil + retAddress, err := types.ParseCIDR(res.Address) + return retAddress, res.Data, err } // ReleaseAddress releases the address from the specified address pool func (a *allocator) ReleaseAddress(poolID string, address net.IP) error { - req := &api.ReleaseAddressRequest{PoolID: poolID, Address: address} + var relAddress string + if address != nil { + relAddress = address.String() + } + req := &api.ReleaseAddressRequest{PoolID: poolID, Address: relAddress} res := &api.ReleaseAddressResponse{} return a.call("ReleaseAddress", req, res) } diff --git a/libnetwork/ipams/remote/remote_test.go b/libnetwork/ipams/remote/remote_test.go new file mode 100644 index 0000000000..06dc32fc02 --- /dev/null +++ b/libnetwork/ipams/remote/remote_test.go @@ -0,0 +1,244 @@ +package remote + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/docker/docker/pkg/plugins" + "github.com/docker/libnetwork/ipamapi" + _ "github.com/docker/libnetwork/testutils" +) + +func decodeToMap(r *http.Request) (res map[string]interface{}, err error) { + err = json.NewDecoder(r.Body).Decode(&res) + return +} + +func handle(t *testing.T, mux *http.ServeMux, method string, h func(map[string]interface{}) interface{}) { + mux.HandleFunc(fmt.Sprintf("/%s.%s", ipamapi.PluginEndpointType, method), func(w http.ResponseWriter, r *http.Request) { + ask, err := decodeToMap(r) + if err != nil { + t.Fatal(err) + } + answer := h(ask) + err = json.NewEncoder(w).Encode(&answer) + if err != nil { + t.Fatal(err) + } + }) +} + +func setupPlugin(t *testing.T, name string, mux *http.ServeMux) func() { + if err := os.MkdirAll("/etc/docker/plugins", 0755); err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(mux) + if server == nil { + t.Fatal("Failed to start a HTTP Server") + } + + if err := ioutil.WriteFile(fmt.Sprintf("/etc/docker/plugins/%s.spec", name), []byte(server.URL), 0644); err != nil { + t.Fatal(err) + } + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, `{"Implements": ["%s"]}`, ipamapi.PluginEndpointType) + }) + + return func() { + if err := os.RemoveAll("/etc/docker/plugins"); err != nil { + t.Fatal(err) + } + server.Close() + } +} + +func TestGetDefaultAddressSpaces(t *testing.T) { + var plugin = "test-ipam-driver-addr-spaces" + + mux := http.NewServeMux() + defer setupPlugin(t, plugin, mux)() + + handle(t, mux, "GetDefaultAddressSpaces", func(msg map[string]interface{}) interface{} { + return map[string]interface{}{ + "LocalDefaultAddressSpace": "white", + "GlobalDefaultAddressSpace": "blue", + } + }) + + p, err := plugins.Get(plugin, ipamapi.PluginEndpointType) + if err != nil { + t.Fatal(err) + } + + d := newAllocator(plugin, p.Client) + + l, g, err := d.(*allocator).GetDefaultAddressSpaces() + if err != nil { + t.Fatal(err) + } + + if l != "white" || g != "blue" { + t.Fatalf("Unexpected default local and global address spaces: %s, %s", l, g) + } +} + +func TestRemoteDriver(t *testing.T) { + var plugin = "test-ipam-driver" + + mux := http.NewServeMux() + defer setupPlugin(t, plugin, mux)() + + handle(t, mux, "GetDefaultAddressSpaces", func(msg map[string]interface{}) interface{} { + return map[string]interface{}{ + "LocalDefaultAddressSpace": "white", + "GlobalDefaultAddressSpace": "blue", + } + }) + + handle(t, mux, "RequestPool", func(msg map[string]interface{}) interface{} { + as := "white" + if v, ok := msg["AddressSpace"]; ok && v.(string) != "" { + as = v.(string) + } + + pl := "172.18.0.0/16" + sp := "" + if v, ok := msg["Pool"]; ok && v.(string) != "" { + pl = v.(string) + } + if v, ok := msg["SubPool"]; ok && v.(string) != "" { + sp = v.(string) + } + pid := fmt.Sprintf("%s/%s", as, pl) + if sp != "" { + pid = fmt.Sprintf("%s/%s", pid, sp) + } + return map[string]interface{}{ + "PoolID": pid, + "Pool": pl, + "Data": map[string]string{"DNS": "8.8.8.8"}, + } + }) + + handle(t, mux, "ReleasePool", func(msg map[string]interface{}) interface{} { + if _, ok := msg["PoolID"]; !ok { + t.Fatalf("Missing PoolID in Release request") + } + return map[string]interface{}{} + }) + + handle(t, mux, "RequestAddress", func(msg map[string]interface{}) interface{} { + if _, ok := msg["PoolID"]; !ok { + t.Fatalf("Missing PoolID in address request") + } + prefAddr := "" + if v, ok := msg["Address"]; ok { + prefAddr = v.(string) + } + ip := prefAddr + if ip == "" { + ip = "172.20.0.34" + } + ip = fmt.Sprintf("%s/16", ip) + return map[string]interface{}{ + "Address": ip, + } + }) + + handle(t, mux, "ReleaseAddress", func(msg map[string]interface{}) interface{} { + if _, ok := msg["PoolID"]; !ok { + t.Fatalf("Missing PoolID in address request") + } + if _, ok := msg["Address"]; !ok { + t.Fatalf("Missing Address in release address request") + } + return map[string]interface{}{} + }) + + p, err := plugins.Get(plugin, ipamapi.PluginEndpointType) + if err != nil { + t.Fatal(err) + } + + d := newAllocator(plugin, p.Client) + + l, g, err := d.(*allocator).GetDefaultAddressSpaces() + if err != nil { + t.Fatal(err) + } + if l != "white" || g != "blue" { + t.Fatalf("Unexpected default local/global address spaces: %s, %s", l, g) + } + + // Request any pool + poolID, pool, _, err := d.RequestPool("white", "", "", nil, false) + if err != nil { + t.Fatal(err) + } + if poolID != "white/172.18.0.0/16" { + t.Fatalf("Unexpected pool id: %s", poolID) + } + if pool == nil || pool.String() != "172.18.0.0/16" { + t.Fatalf("Unexpected pool: %s", pool) + } + + // Request specific pool + poolID2, pool2, ops, err := d.RequestPool("white", "172.20.0.0/16", "", nil, false) + if err != nil { + t.Fatal(err) + } + if poolID2 != "white/172.20.0.0/16" { + t.Fatalf("Unexpected pool id: %s", poolID2) + } + if pool2 == nil || pool2.String() != "172.20.0.0/16" { + t.Fatalf("Unexpected pool: %s", pool2) + } + if dns, ok := ops["DNS"]; !ok || dns != "8.8.8.8" { + t.Fatalf("Missing options") + } + + // Request specific pool and subpool + poolID3, pool3, _, err := d.RequestPool("white", "172.20.0.0/16", "172.20.3.0/24" /*nil*/, map[string]string{"culo": "yes"}, false) + if err != nil { + t.Fatal(err) + } + if poolID3 != "white/172.20.0.0/16/172.20.3.0/24" { + t.Fatalf("Unexpected pool id: %s", poolID3) + } + if pool3 == nil || pool3.String() != "172.20.0.0/16" { + t.Fatalf("Unexpected pool: %s", pool3) + } + + // Request any address + addr, _, err := d.RequestAddress(poolID2, nil, nil) + if err != nil { + t.Fatal(err) + } + if addr == nil || addr.String() != "172.20.0.34/16" { + t.Fatalf("Unexpected address: %s", addr) + } + + // Request specific address + addr2, _, err := d.RequestAddress(poolID2, net.ParseIP("172.20.1.45"), nil) + if err != nil { + t.Fatal(err) + } + if addr2 == nil || addr2.String() != "172.20.1.45/16" { + t.Fatalf("Unexpected address: %s", addr2) + } + + // Release address + err = d.ReleaseAddress(poolID, net.ParseIP("172.18.1.45")) + if err != nil { + t.Fatal(err) + } +}