As of https://github.com/docker/swarmkit/pull/1607, swarmkit honors
global network plugins while allocating network resources.
This IT covers the e2e integration between libnetwork, swarmkit and
docker engine to support global network-plugins for swarm-mode
Signed-off-by: Madhu Venugopal <madhu@docker.com>
| ... | ... |
@@ -2,6 +2,7 @@ package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "net/http/httptest" |
|
| 5 | 6 |
"os" |
| 6 | 7 |
"path/filepath" |
| 7 | 8 |
"sync" |
| ... | ... |
@@ -240,6 +241,7 @@ func init() {
|
| 240 | 240 |
} |
| 241 | 241 |
|
| 242 | 242 |
type DockerSwarmSuite struct {
|
| 243 |
+ server *httptest.Server |
|
| 243 | 244 |
ds *DockerSuite |
| 244 | 245 |
daemons []*SwarmDaemon |
| 245 | 246 |
daemonsLock sync.Mutex // protect access to daemons |
| ... | ... |
@@ -3,13 +3,22 @@ |
| 3 | 3 |
package main |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
+ "encoding/json" |
|
| 7 |
+ "fmt" |
|
| 6 | 8 |
"io/ioutil" |
| 9 |
+ "net/http" |
|
| 10 |
+ "net/http/httptest" |
|
| 11 |
+ "os" |
|
| 7 | 12 |
"strings" |
| 8 | 13 |
"time" |
| 9 | 14 |
|
| 10 | 15 |
"github.com/docker/docker/api/types/swarm" |
| 11 | 16 |
"github.com/docker/docker/pkg/integration/checker" |
| 17 |
+ "github.com/docker/libnetwork/driverapi" |
|
| 18 |
+ "github.com/docker/libnetwork/ipamapi" |
|
| 19 |
+ remoteipam "github.com/docker/libnetwork/ipams/remote/api" |
|
| 12 | 20 |
"github.com/go-check/check" |
| 21 |
+ "github.com/vishvananda/netlink" |
|
| 13 | 22 |
) |
| 14 | 23 |
|
| 15 | 24 |
func (s *DockerSwarmSuite) TestSwarmUpdate(c *check.C) {
|
| ... | ... |
@@ -364,3 +373,198 @@ func (s *DockerSwarmSuite) TestPsListContainersFilterIsTask(c *check.C) {
|
| 364 | 364 |
c.Assert(lines, checker.HasLen, 1) |
| 365 | 365 |
c.Assert(lines[0], checker.Not(checker.Equals), bareID, check.Commentf("Expected not %s, but got it for is-task label, output %q", bareID, out))
|
| 366 | 366 |
} |
| 367 |
+ |
|
| 368 |
+const globalNetworkPlugin = "global-network-plugin" |
|
| 369 |
+const globalIPAMPlugin = "global-ipam-plugin" |
|
| 370 |
+ |
|
| 371 |
+func (s *DockerSwarmSuite) SetUpSuite(c *check.C) {
|
|
| 372 |
+ mux := http.NewServeMux() |
|
| 373 |
+ s.server = httptest.NewServer(mux) |
|
| 374 |
+ c.Assert(s.server, check.NotNil, check.Commentf("Failed to start an HTTP Server"))
|
|
| 375 |
+ setupRemoteGlobalNetworkPlugin(c, mux, s.server.URL, globalNetworkPlugin, globalIPAMPlugin) |
|
| 376 |
+} |
|
| 377 |
+ |
|
| 378 |
+func setupRemoteGlobalNetworkPlugin(c *check.C, mux *http.ServeMux, url, netDrv, ipamDrv string) {
|
|
| 379 |
+ |
|
| 380 |
+ mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
|
|
| 381 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 382 |
+ fmt.Fprintf(w, `{"Implements": ["%s", "%s"]}`, driverapi.NetworkPluginEndpointType, ipamapi.PluginEndpointType)
|
|
| 383 |
+ }) |
|
| 384 |
+ |
|
| 385 |
+ // Network driver implementation |
|
| 386 |
+ mux.HandleFunc(fmt.Sprintf("/%s.GetCapabilities", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 387 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 388 |
+ fmt.Fprintf(w, `{"Scope":"global"}`)
|
|
| 389 |
+ }) |
|
| 390 |
+ |
|
| 391 |
+ mux.HandleFunc(fmt.Sprintf("/%s.AllocateNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 392 |
+ err := json.NewDecoder(r.Body).Decode(&remoteDriverNetworkRequest) |
|
| 393 |
+ if err != nil {
|
|
| 394 |
+ http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) |
|
| 395 |
+ return |
|
| 396 |
+ } |
|
| 397 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 398 |
+ fmt.Fprintf(w, "null") |
|
| 399 |
+ }) |
|
| 400 |
+ |
|
| 401 |
+ mux.HandleFunc(fmt.Sprintf("/%s.FreeNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 402 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 403 |
+ fmt.Fprintf(w, "null") |
|
| 404 |
+ }) |
|
| 405 |
+ |
|
| 406 |
+ mux.HandleFunc(fmt.Sprintf("/%s.CreateNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 407 |
+ err := json.NewDecoder(r.Body).Decode(&remoteDriverNetworkRequest) |
|
| 408 |
+ if err != nil {
|
|
| 409 |
+ http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) |
|
| 410 |
+ return |
|
| 411 |
+ } |
|
| 412 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 413 |
+ fmt.Fprintf(w, "null") |
|
| 414 |
+ }) |
|
| 415 |
+ |
|
| 416 |
+ mux.HandleFunc(fmt.Sprintf("/%s.DeleteNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 417 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 418 |
+ fmt.Fprintf(w, "null") |
|
| 419 |
+ }) |
|
| 420 |
+ |
|
| 421 |
+ mux.HandleFunc(fmt.Sprintf("/%s.CreateEndpoint", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 422 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 423 |
+ fmt.Fprintf(w, `{"Interface":{"MacAddress":"a0:b1:c2:d3:e4:f5"}}`)
|
|
| 424 |
+ }) |
|
| 425 |
+ |
|
| 426 |
+ mux.HandleFunc(fmt.Sprintf("/%s.Join", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 427 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 428 |
+ |
|
| 429 |
+ veth := &netlink.Veth{
|
|
| 430 |
+ LinkAttrs: netlink.LinkAttrs{Name: "randomIfName", TxQLen: 0}, PeerName: "cnt0"}
|
|
| 431 |
+ if err := netlink.LinkAdd(veth); err != nil {
|
|
| 432 |
+ fmt.Fprintf(w, `{"Error":"failed to add veth pair: `+err.Error()+`"}`)
|
|
| 433 |
+ } else {
|
|
| 434 |
+ fmt.Fprintf(w, `{"InterfaceName":{ "SrcName":"cnt0", "DstPrefix":"veth"}}`)
|
|
| 435 |
+ } |
|
| 436 |
+ }) |
|
| 437 |
+ |
|
| 438 |
+ mux.HandleFunc(fmt.Sprintf("/%s.Leave", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 439 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 440 |
+ fmt.Fprintf(w, "null") |
|
| 441 |
+ }) |
|
| 442 |
+ |
|
| 443 |
+ mux.HandleFunc(fmt.Sprintf("/%s.DeleteEndpoint", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 444 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 445 |
+ if link, err := netlink.LinkByName("cnt0"); err == nil {
|
|
| 446 |
+ netlink.LinkDel(link) |
|
| 447 |
+ } |
|
| 448 |
+ fmt.Fprintf(w, "null") |
|
| 449 |
+ }) |
|
| 450 |
+ |
|
| 451 |
+ // IPAM Driver implementation |
|
| 452 |
+ var ( |
|
| 453 |
+ poolRequest remoteipam.RequestPoolRequest |
|
| 454 |
+ poolReleaseReq remoteipam.ReleasePoolRequest |
|
| 455 |
+ addressRequest remoteipam.RequestAddressRequest |
|
| 456 |
+ addressReleaseReq remoteipam.ReleaseAddressRequest |
|
| 457 |
+ lAS = "localAS" |
|
| 458 |
+ gAS = "globalAS" |
|
| 459 |
+ pool = "172.28.0.0/16" |
|
| 460 |
+ poolID = lAS + "/" + pool |
|
| 461 |
+ gw = "172.28.255.254/16" |
|
| 462 |
+ ) |
|
| 463 |
+ |
|
| 464 |
+ mux.HandleFunc(fmt.Sprintf("/%s.GetDefaultAddressSpaces", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 465 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 466 |
+ fmt.Fprintf(w, `{"LocalDefaultAddressSpace":"`+lAS+`", "GlobalDefaultAddressSpace": "`+gAS+`"}`)
|
|
| 467 |
+ }) |
|
| 468 |
+ |
|
| 469 |
+ mux.HandleFunc(fmt.Sprintf("/%s.RequestPool", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 470 |
+ err := json.NewDecoder(r.Body).Decode(&poolRequest) |
|
| 471 |
+ if err != nil {
|
|
| 472 |
+ http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) |
|
| 473 |
+ return |
|
| 474 |
+ } |
|
| 475 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 476 |
+ if poolRequest.AddressSpace != lAS && poolRequest.AddressSpace != gAS {
|
|
| 477 |
+ fmt.Fprintf(w, `{"Error":"Unknown address space in pool request: `+poolRequest.AddressSpace+`"}`)
|
|
| 478 |
+ } else if poolRequest.Pool != "" && poolRequest.Pool != pool {
|
|
| 479 |
+ fmt.Fprintf(w, `{"Error":"Cannot handle explicit pool requests yet"}`)
|
|
| 480 |
+ } else {
|
|
| 481 |
+ fmt.Fprintf(w, `{"PoolID":"`+poolID+`", "Pool":"`+pool+`"}`)
|
|
| 482 |
+ } |
|
| 483 |
+ }) |
|
| 484 |
+ |
|
| 485 |
+ mux.HandleFunc(fmt.Sprintf("/%s.RequestAddress", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 486 |
+ err := json.NewDecoder(r.Body).Decode(&addressRequest) |
|
| 487 |
+ if err != nil {
|
|
| 488 |
+ http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) |
|
| 489 |
+ return |
|
| 490 |
+ } |
|
| 491 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 492 |
+ // make sure libnetwork is now querying on the expected pool id |
|
| 493 |
+ if addressRequest.PoolID != poolID {
|
|
| 494 |
+ fmt.Fprintf(w, `{"Error":"unknown pool id"}`)
|
|
| 495 |
+ } else if addressRequest.Address != "" {
|
|
| 496 |
+ fmt.Fprintf(w, `{"Error":"Cannot handle explicit address requests yet"}`)
|
|
| 497 |
+ } else {
|
|
| 498 |
+ fmt.Fprintf(w, `{"Address":"`+gw+`"}`)
|
|
| 499 |
+ } |
|
| 500 |
+ }) |
|
| 501 |
+ |
|
| 502 |
+ mux.HandleFunc(fmt.Sprintf("/%s.ReleaseAddress", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 503 |
+ err := json.NewDecoder(r.Body).Decode(&addressReleaseReq) |
|
| 504 |
+ if err != nil {
|
|
| 505 |
+ http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) |
|
| 506 |
+ return |
|
| 507 |
+ } |
|
| 508 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 509 |
+ // make sure libnetwork is now asking to release the expected address from the expected poolid |
|
| 510 |
+ if addressRequest.PoolID != poolID {
|
|
| 511 |
+ fmt.Fprintf(w, `{"Error":"unknown pool id"}`)
|
|
| 512 |
+ } else if addressReleaseReq.Address != gw {
|
|
| 513 |
+ fmt.Fprintf(w, `{"Error":"unknown address"}`)
|
|
| 514 |
+ } else {
|
|
| 515 |
+ fmt.Fprintf(w, "null") |
|
| 516 |
+ } |
|
| 517 |
+ }) |
|
| 518 |
+ |
|
| 519 |
+ mux.HandleFunc(fmt.Sprintf("/%s.ReleasePool", ipamapi.PluginEndpointType), func(w http.ResponseWriter, r *http.Request) {
|
|
| 520 |
+ err := json.NewDecoder(r.Body).Decode(&poolReleaseReq) |
|
| 521 |
+ if err != nil {
|
|
| 522 |
+ http.Error(w, "Unable to decode JSON payload: "+err.Error(), http.StatusBadRequest) |
|
| 523 |
+ return |
|
| 524 |
+ } |
|
| 525 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json")
|
|
| 526 |
+ // make sure libnetwork is now asking to release the expected poolid |
|
| 527 |
+ if addressRequest.PoolID != poolID {
|
|
| 528 |
+ fmt.Fprintf(w, `{"Error":"unknown pool id"}`)
|
|
| 529 |
+ } else {
|
|
| 530 |
+ fmt.Fprintf(w, "null") |
|
| 531 |
+ } |
|
| 532 |
+ }) |
|
| 533 |
+ |
|
| 534 |
+ err := os.MkdirAll("/etc/docker/plugins", 0755)
|
|
| 535 |
+ c.Assert(err, checker.IsNil) |
|
| 536 |
+ |
|
| 537 |
+ fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", netDrv)
|
|
| 538 |
+ err = ioutil.WriteFile(fileName, []byte(url), 0644) |
|
| 539 |
+ c.Assert(err, checker.IsNil) |
|
| 540 |
+ |
|
| 541 |
+ ipamFileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", ipamDrv)
|
|
| 542 |
+ err = ioutil.WriteFile(ipamFileName, []byte(url), 0644) |
|
| 543 |
+ c.Assert(err, checker.IsNil) |
|
| 544 |
+} |
|
| 545 |
+ |
|
| 546 |
+func (s *DockerSwarmSuite) TestSwarmNetworkPlugin(c *check.C) {
|
|
| 547 |
+ d := s.AddDaemon(c, true, true) |
|
| 548 |
+ |
|
| 549 |
+ out, err := d.Cmd("network", "create", "-d", globalNetworkPlugin, "foo")
|
|
| 550 |
+ c.Assert(err, checker.IsNil) |
|
| 551 |
+ c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") |
|
| 552 |
+ |
|
| 553 |
+ name := "top" |
|
| 554 |
+ out, err = d.Cmd("service", "create", "--name", name, "--network", "foo", "busybox", "top")
|
|
| 555 |
+ c.Assert(err, checker.IsNil) |
|
| 556 |
+ c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") |
|
| 557 |
+ |
|
| 558 |
+ out, err = d.Cmd("service", "inspect", "--format", "{{range .Spec.Networks}}{{.Target}}{{end}}", name)
|
|
| 559 |
+ c.Assert(err, checker.IsNil) |
|
| 560 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, "foo") |
|
| 561 |
+} |