diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2b1e8c70a5..1b3111b2dc 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -118,7 +118,7 @@ }, { "ImportPath": "github.com/samalba/dockerclient", - "Rev": "73edd1c3a9d280bcec6a22a12fc90c1a46558b4e" + "Rev": "32a9231a6d93f563010c5ffc2f6fb223b347b6b1" }, { "ImportPath": "github.com/samuel/go-zookeeper/zk", diff --git a/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient.go b/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient.go index 81428b3cf8..c4302d539e 100644 --- a/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient.go +++ b/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient.go @@ -778,3 +778,79 @@ func (client *DockerClient) CreateVolume(request *VolumeCreateRequest) (*Volume, err = json.Unmarshal(data, volume) return volume, err } + +func (client *DockerClient) ListNetworks(filters string) ([]*NetworkResource, error) { + uri := fmt.Sprintf("/%s/networks", APIVersion) + + if filters != "" { + uri += "&filters=" + filters + } + + data, err := client.doRequest("GET", uri, nil, nil) + if err != nil { + return nil, err + } + ret := []*NetworkResource{} + err = json.Unmarshal(data, &ret) + if err != nil { + return nil, err + } + return ret, nil +} + +func (client *DockerClient) InspectNetwork(id string) (*NetworkResource, error) { + uri := fmt.Sprintf("/%s/networks/%s", APIVersion, id) + + data, err := client.doRequest("GET", uri, nil, nil) + if err != nil { + return nil, err + } + ret := &NetworkResource{} + err = json.Unmarshal(data, ret) + if err != nil { + return nil, err + } + + return ret, nil +} + +func (client *DockerClient) CreateNetwork(config *NetworkCreate) (*NetworkCreateResponse, error) { + data, err := json.Marshal(config) + if err != nil { + return nil, err + } + uri := fmt.Sprintf("/%s/networks/create", APIVersion) + data, err = client.doRequest("POST", uri, data, nil) + if err != nil { + return nil, err + } + ret := &NetworkCreateResponse{} + err = json.Unmarshal(data, ret) + return ret, nil +} + +func (client *DockerClient) ConnectNetwork(id, container string) error { + data, err := json.Marshal(NetworkConnect{Container: container}) + if err != nil { + return err + } + uri := fmt.Sprintf("/%s/networks/%s/connect", APIVersion, id) + _, err = client.doRequest("POST", uri, data, nil) + return err +} + +func (client *DockerClient) DisconnectNetwork(id, container string) error { + data, err := json.Marshal(NetworkDisconnect{Container: container}) + if err != nil { + return err + } + uri := fmt.Sprintf("/%s/networks/%s/disconnect", APIVersion, id) + _, err = client.doRequest("POST", uri, data, nil) + return err +} + +func (client *DockerClient) RemoveNetwork(id string) error { + uri := fmt.Sprintf("/%s/networks/%s", APIVersion, id) + _, err := client.doRequest("DELETE", uri, nil, nil) + return err +} diff --git a/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient_test.go b/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient_test.go index 88257e01b7..afc1b071b0 100644 --- a/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient_test.go +++ b/Godeps/_workspace/src/github.com/samalba/dockerclient/dockerclient_test.go @@ -119,6 +119,7 @@ func TestListContainersWithSize(t *testing.T) { cnt := containers[0] assertEqual(t, cnt.SizeRw, int64(123), "") } + func TestListContainersWithFilters(t *testing.T) { client := testDockerClient(t) containers, err := client.ListContainers(true, true, "{'id':['332375cfbc23edb921a21026314c3497674ba8bdcb2c85e0e65ebf2017f688ce']}") diff --git a/Godeps/_workspace/src/github.com/samalba/dockerclient/interface.go b/Godeps/_workspace/src/github.com/samalba/dockerclient/interface.go index b173fc1171..4215ca4e2f 100644 --- a/Godeps/_workspace/src/github.com/samalba/dockerclient/interface.go +++ b/Godeps/_workspace/src/github.com/samalba/dockerclient/interface.go @@ -48,4 +48,10 @@ type Client interface { ListVolumes() ([]*Volume, error) RemoveVolume(name string) error CreateVolume(request *VolumeCreateRequest) (*Volume, error) + ListNetworks(filters string) ([]*NetworkResource, error) + InspectNetwork(id string) (*NetworkResource, error) + CreateNetwork(config *NetworkCreate) (*NetworkCreateResponse, error) + ConnectNetwork(id, container string) error + DisconnectNetwork(id, container string) error + RemoveNetwork(id string) error } diff --git a/Godeps/_workspace/src/github.com/samalba/dockerclient/mockclient/mock.go b/Godeps/_workspace/src/github.com/samalba/dockerclient/mockclient/mock.go index b12195d0f8..9944512e93 100644 --- a/Godeps/_workspace/src/github.com/samalba/dockerclient/mockclient/mock.go +++ b/Godeps/_workspace/src/github.com/samalba/dockerclient/mockclient/mock.go @@ -185,3 +185,33 @@ func (client *MockClient) CreateVolume(request *dockerclient.VolumeCreateRequest args := client.Mock.Called(request) return args.Get(0).(*dockerclient.Volume), args.Error(1) } + +func (client *MockClient) ListNetworks(filters string) ([]*dockerclient.NetworkResource, error) { + args := client.Mock.Called(filters) + return args.Get(0).([]*dockerclient.NetworkResource), args.Error(1) +} + +func (client *MockClient) InspectNetwork(id string) (*dockerclient.NetworkResource, error) { + args := client.Mock.Called(id) + return args.Get(0).(*dockerclient.NetworkResource), args.Error(1) +} + +func (client *MockClient) CreateNetwork(config *dockerclient.NetworkCreate) (*dockerclient.NetworkCreateResponse, error) { + args := client.Mock.Called(config) + return args.Get(0).(*dockerclient.NetworkCreateResponse), args.Error(1) +} + +func (client *MockClient) ConnectNetwork(id, container string) error { + args := client.Mock.Called(id, container) + return args.Error(0) +} + +func (client *MockClient) DisconnectNetwork(id, container string) error { + args := client.Mock.Called(id, container) + return args.Error(0) +} + +func (client *MockClient) RemoveNetwork(id string) error { + args := client.Mock.Called(id) + return args.Error(0) +} diff --git a/Godeps/_workspace/src/github.com/samalba/dockerclient/nopclient/nop.go b/Godeps/_workspace/src/github.com/samalba/dockerclient/nopclient/nop.go index 5763509599..ef64536304 100644 --- a/Godeps/_workspace/src/github.com/samalba/dockerclient/nopclient/nop.go +++ b/Godeps/_workspace/src/github.com/samalba/dockerclient/nopclient/nop.go @@ -153,6 +153,31 @@ func (client *NopClient) ListVolumes() ([]*dockerclient.Volume, error) { func (client *NopClient) RemoveVolume(name string) error { return ErrNoEngine } + func (client *NopClient) CreateVolume(request *dockerclient.VolumeCreateRequest) (*dockerclient.Volume, error) { return nil, ErrNoEngine } + +func (client *NopClient) ListNetworks(filters string) ([]*dockerclient.NetworkResource, error) { + return nil, ErrNoEngine +} + +func (client *NopClient) InspectNetwork(id string) (*dockerclient.NetworkResource, error) { + return nil, ErrNoEngine +} + +func (client *NopClient) CreateNetwork(config *dockerclient.NetworkCreate) (*dockerclient.NetworkCreateResponse, error) { + return nil, ErrNoEngine +} + +func (client *NopClient) ConnectNetwork(id, container string) error { + return ErrNoEngine +} + +func (client *NopClient) DisconnectNetwork(id, container string) error { + return ErrNoEngine +} + +func (client *NopClient) RemoveNetwork(id string) error { + return ErrNoEngine +} diff --git a/Godeps/_workspace/src/github.com/samalba/dockerclient/types.go b/Godeps/_workspace/src/github.com/samalba/dockerclient/types.go index a1654b61a5..b1f5a006c3 100644 --- a/Godeps/_workspace/src/github.com/samalba/dockerclient/types.go +++ b/Godeps/_workspace/src/github.com/samalba/dockerclient/types.go @@ -460,3 +460,44 @@ type VolumeCreateRequest struct { Driver string // Driver is the name of the driver that should be used to create the volume DriverOpts map[string]string // DriverOpts holds the driver specific options to use for when creating the volume. } + +// NetworkResource is the body of the "get network" http response message +type NetworkResource struct { + Name string `json:"name"` + ID string `json:"id"` + Driver string `json:"driver"` + Containers map[string]EndpointResource `json:"containers"` + Options map[string]interface{} `json:"options,omitempty"` +} + +//EndpointResource contains network resources allocated and usd for a container in a network +type EndpointResource struct { + EndpointID string `json:"endpoint"` + MacAddress string `json:"mac_address"` + IPv4Address string `json:"ipv4_address"` + IPv6Address string `json:"ipv6_address"` +} + +// NetworkCreate is the expected body of the "create network" http request message +type NetworkCreate struct { + Name string `json:"name"` + CheckDuplicate bool `json:"check_duplicate"` + Driver string `json:"driver"` + Options map[string]interface{} `json:"options"` +} + +// NetworkCreateResponse is the response message sent by the server for network create call +type NetworkCreateResponse struct { + ID string `json:"id"` + Warning string `json:"warning"` +} + +// NetworkConnect represents the data to be used to connect a container to the network +type NetworkConnect struct { + Container string `json:"container"` +} + +// NetworkDisconnect represents the data to be used to disconnect a container from the network +type NetworkDisconnect struct { + Container string `json:"container"` +} diff --git a/api/handlers.go b/api/handlers.go index c92e74eb2e..4d6d0a8df6 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -169,6 +169,18 @@ func getImagesJSON(c *context, w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(images) } +// GET /networks +func getNetworks(c *context, w http.ResponseWriter, r *http.Request) { + out := []*dockerclient.NetworkResource{} + for _, network := range c.cluster.Networks() { + tmp := (*network).NetworkResource + tmp.Name = network.Engine.Name + "/" + network.Name + out = append(out, &tmp) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(out) +} + // GET /volumes func getVolumes(c *context, w http.ResponseWriter, r *http.Request) { volumes := struct { @@ -415,6 +427,29 @@ func deleteContainers(c *context, w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +// POST /networks/create +func postNetworksCreate(c *context, w http.ResponseWriter, r *http.Request) { + var request dockerclient.NetworkCreate + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + httpError(w, err.Error(), http.StatusBadRequest) + return + } + + if request.Driver == "" { + request.Driver = "overlay" + } + + response, err := c.cluster.CreateNetwork(&request) + if err != nil { + httpError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + // POST /volumes func postVolumes(c *context, w http.ResponseWriter, r *http.Request) { var request dockerclient.VolumeCreateRequest @@ -637,6 +672,27 @@ func deleteImages(c *context, w http.ResponseWriter, r *http.Request) { json.NewEncoder(NewWriteFlusher(w)).Encode(out) } +// DELETE /networks/{networkid:.*} +func deleteNetworks(c *context, w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + httpError(w, err.Error(), http.StatusInternalServerError) + return + } + + var id = mux.Vars(r)["networkid"] + + if network := c.cluster.Networks().Get(id); network != nil { + if err := c.cluster.RemoveNetwork(network); err != nil { + httpError(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + httpError(w, fmt.Sprintf("No such network %s", id), http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) +} + // DELETE /volumes/{names:.*} func deleteVolumes(c *context, w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { @@ -663,6 +719,20 @@ func ping(c *context, w http.ResponseWriter, r *http.Request) { w.Write([]byte{'O', 'K'}) } +// Proxy a request to the right node +func proxyNetwork(c *context, w http.ResponseWriter, r *http.Request) { + var id = mux.Vars(r)["networkid"] + if network := c.cluster.Networks().Get(id); network != nil { + + // Set the network ID in the proxied URL path. + r.URL.Path = strings.Replace(r.URL.Path, id, network.ID, 1) + + proxy(c.tlsConfig, network.Engine.Addr, w, r) + return + } + httpError(w, fmt.Sprintf("No such network: %s", id), http.StatusNotFound) +} + // Proxy a request to the right node func proxyVolume(c *context, w http.ResponseWriter, r *http.Request) { var name = mux.Vars(r)["volumename"] diff --git a/api/primary.go b/api/primary.go index cc7fd2669b..314f356f8c 100644 --- a/api/primary.go +++ b/api/primary.go @@ -47,41 +47,47 @@ var routes = map[string]map[string]handler{ "/containers/{name:.*}/stats": proxyContainer, "/containers/{name:.*}/attach/ws": proxyHijack, "/exec/{execid:.*}/json": proxyContainer, + "/networks": getNetworks, + "/networks/{networkid:.*}": proxyNetwork, "/volumes": getVolumes, "/volumes/{volumename:.*}": proxyVolume, }, "POST": { - "/auth": proxyRandom, - "/commit": postCommit, - "/build": postBuild, - "/images/create": postImagesCreate, - "/images/load": postImagesLoad, - "/images/{name:.*}/push": proxyImageTagOptional, - "/images/{name:.*}/tag": postTagImage, - "/containers/create": postContainersCreate, - "/containers/{name:.*}/kill": proxyContainerAndForceRefresh, - "/containers/{name:.*}/pause": proxyContainerAndForceRefresh, - "/containers/{name:.*}/unpause": proxyContainerAndForceRefresh, - "/containers/{name:.*}/rename": postRenameContainer, - "/containers/{name:.*}/restart": proxyContainerAndForceRefresh, - "/containers/{name:.*}/start": proxyContainerAndForceRefresh, - "/containers/{name:.*}/stop": proxyContainerAndForceRefresh, - "/containers/{name:.*}/wait": proxyContainerAndForceRefresh, - "/containers/{name:.*}/resize": proxyContainer, - "/containers/{name:.*}/attach": proxyHijack, - "/containers/{name:.*}/copy": proxyContainer, - "/containers/{name:.*}/exec": postContainersExec, - "/exec/{execid:.*}/start": postExecStart, - "/exec/{execid:.*}/resize": proxyContainer, - "/volumes": postVolumes, + "/auth": proxyRandom, + "/commit": postCommit, + "/build": postBuild, + "/images/create": postImagesCreate, + "/images/load": postImagesLoad, + "/images/{name:.*}/push": proxyImageTagOptional, + "/images/{name:.*}/tag": postTagImage, + "/containers/create": postContainersCreate, + "/containers/{name:.*}/kill": proxyContainerAndForceRefresh, + "/containers/{name:.*}/pause": proxyContainerAndForceRefresh, + "/containers/{name:.*}/unpause": proxyContainerAndForceRefresh, + "/containers/{name:.*}/rename": postRenameContainer, + "/containers/{name:.*}/restart": proxyContainerAndForceRefresh, + "/containers/{name:.*}/start": proxyContainerAndForceRefresh, + "/containers/{name:.*}/stop": proxyContainerAndForceRefresh, + "/containers/{name:.*}/wait": proxyContainerAndForceRefresh, + "/containers/{name:.*}/resize": proxyContainer, + "/containers/{name:.*}/attach": proxyHijack, + "/containers/{name:.*}/copy": proxyContainer, + "/containers/{name:.*}/exec": postContainersExec, + "/exec/{execid:.*}/start": postExecStart, + "/exec/{execid:.*}/resize": proxyContainer, + "/networks/create": postNetworksCreate, + "/networks/{networkid:.*}/connect": proxyNetwork, + "/networks/{networkid:.*}/disconnect": proxyNetwork, + "/volumes": postVolumes, }, "PUT": { "/containers/{name:.*}/archive": proxyContainer, }, "DELETE": { - "/containers/{name:.*}": deleteContainers, - "/images/{name:.*}": deleteImages, - "/volumes/{name:.*}": deleteVolumes, + "/containers/{name:.*}": deleteContainers, + "/images/{name:.*}": deleteImages, + "/networks/{networkid:.*}": deleteNetworks, + "/volumes/{name:.*}": deleteVolumes, }, "OPTIONS": { "": optionsHandler, diff --git a/cluster/cluster.go b/cluster/cluster.go index f29db15dfd..7b23c4cebd 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -32,6 +32,15 @@ type Cluster interface { // cluster.Containers().Get(IDOrName) Container(IDOrName string) *Container + // Return all networks + Networks() Networks + + // Create a network + CreateNetwork(request *dockerclient.NetworkCreate) (*dockerclient.NetworkCreateResponse, error) + + // Remove a network from the cluster + RemoveNetwork(network *Network) error + // Create a volume CreateVolume(request *dockerclient.VolumeCreateRequest) (*Volume, error) diff --git a/cluster/engine.go b/cluster/engine.go index 7e2262558c..08dc9b75e0 100644 --- a/cluster/engine.go +++ b/cluster/engine.go @@ -73,6 +73,7 @@ type Engine struct { refreshDelayer *delayer containers map[string]*Container images []*Image + networks map[string]*Network volumes map[string]*Volume client dockerclient.Client eventHandler EventHandler @@ -89,6 +90,7 @@ func NewEngine(addr string, overcommitRatio float64) *Engine { Labels: make(map[string]string), stopCh: make(chan struct{}), containers: make(map[string]*Container), + networks: make(map[string]*Network), volumes: make(map[string]*Volume), healthy: true, overcommitRatio: int64(overcommitRatio * 100), @@ -138,6 +140,7 @@ func (e *Engine) ConnectWithClient(client dockerclient.Client) error { // Do not check error as older daemon don't support this call e.RefreshVolumes() + e.RefreshNetworks() // Start the update loop. go e.refreshLoop() @@ -219,6 +222,13 @@ func (e *Engine) RemoveImage(image *Image, name string, force bool) ([]*dockercl return e.client.RemoveImage(name, force) } +// RemoveNetwork deletes a network from the engine. +func (e *Engine) RemoveNetwork(network *Network) error { + err := e.client.RemoveNetwork(network.ID) + e.RefreshNetworks() + return err +} + // RemoveVolume deletes a volume from the engine. func (e *Engine) RemoveVolume(name string) error { if err := e.client.RemoveVolume(name); err != nil { @@ -249,6 +259,21 @@ func (e *Engine) RefreshImages() error { return nil } +// RefreshNetworks refreshes the list of networks on the engine. +func (e *Engine) RefreshNetworks() error { + networks, err := e.client.ListNetworks("") + if err != nil { + return err + } + e.Lock() + e.networks = make(map[string]*Network) + for _, network := range networks { + e.networks[network.ID] = &Network{NetworkResource: *network, Engine: e} + } + e.Unlock() + return nil +} + // RefreshVolumes refreshes the list of volumes on the engine. func (e *Engine) RefreshVolumes() error { volumes, err := e.client.ListVolumes() @@ -382,6 +407,7 @@ func (e *Engine) refreshLoop() { if err == nil { // Do not check error as older daemon don't support this call e.RefreshVolumes() + e.RefreshNetworks() err = e.RefreshImages() } @@ -496,6 +522,7 @@ func (e *Engine) Create(config *ContainerConfig, name string, pullImage bool) (* // Register the container immediately while waiting for a state refresh. // Force a state refresh to pick up the newly created container. e.refreshContainer(id, true) + e.RefreshNetworks() e.RLock() defer e.RUnlock() @@ -522,6 +549,15 @@ func (e *Engine) RemoveContainer(container *Container, force, volumes bool) erro return nil } +// CreateNetwork creates a network in the engine +func (e *Engine) CreateNetwork(request *dockerclient.NetworkCreate) (*dockerclient.NetworkCreateResponse, error) { + response, err := e.client.CreateNetwork(request) + + e.RefreshNetworks() + + return response, err +} + // CreateVolume creates a volume in the engine func (e *Engine) CreateVolume(request *dockerclient.VolumeCreateRequest) (*Volume, error) { volume, err := e.client.CreateVolume(request) @@ -621,6 +657,18 @@ func (e *Engine) Images(all bool, filters dockerfilters.Args) []*Image { return images } +// Networks returns all the networks in the engine +func (e *Engine) Networks() Networks { + e.RLock() + + networks := Networks{} + for _, network := range e.networks { + networks = append(networks, network) + } + e.RUnlock() + return networks +} + // Volumes returns all the volumes in the engine func (e *Engine) Volumes() []*Volume { e.RLock() @@ -662,10 +710,12 @@ func (e *Engine) handler(ev *dockerclient.Event, _ chan error, args ...interface // order to update container.Info and get the new NetworkSettings. e.refreshContainer(ev.Id, true) e.RefreshVolumes() + e.RefreshNetworks() default: // Otherwise, do a "soft" refresh of the container. e.refreshContainer(ev.Id, false) e.RefreshVolumes() + e.RefreshNetworks() } // If there is no event handler registered, abort right now. diff --git a/cluster/engine_test.go b/cluster/engine_test.go index 80e15c1b34..a7d845d246 100644 --- a/cluster/engine_test.go +++ b/cluster/engine_test.go @@ -75,6 +75,7 @@ func TestEngineCpusMemory(t *testing.T) { client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil) client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil) client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return() assert.NoError(t, engine.ConnectWithClient(client)) @@ -97,6 +98,7 @@ func TestEngineSpecs(t *testing.T) { client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil) client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil) client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return() assert.NoError(t, engine.ConnectWithClient(client)) @@ -127,6 +129,7 @@ func TestEngineState(t *testing.T) { client.On("ListContainers", true, false, "").Return([]dockerclient.Container{{Id: "one"}}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil).Once() client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("InspectContainer", "one").Return(&dockerclient.ContainerInfo{Config: &dockerclient.ContainerConfig{CpuShares: 100}}, nil).Once() client.On("ListContainers", true, false, fmt.Sprintf("{%q:[%q]}", "id", "two")).Return([]dockerclient.Container{{Id: "two"}}, nil).Once() client.On("InspectContainer", "two").Return(&dockerclient.ContainerInfo{Config: &dockerclient.ContainerConfig{CpuShares: 100}}, nil).Once() @@ -173,6 +176,7 @@ func TestCreateContainer(t *testing.T) { client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil).Once() client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) assert.NoError(t, engine.ConnectWithClient(client)) assert.True(t, engine.isConnected()) @@ -187,6 +191,7 @@ func TestCreateContainer(t *testing.T) { client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil).Once() client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once() container, err := engine.Create(config, name, false) assert.Nil(t, err) @@ -211,6 +216,7 @@ func TestCreateContainer(t *testing.T) { client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil).Once() client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once() container, err = engine.Create(config, name, true) assert.Nil(t, err) @@ -288,6 +294,7 @@ func TestUsedCpus(t *testing.T) { client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil).Once() client.On("ListContainers", true, false, "").Return([]dockerclient.Container{{Id: "test"}}, nil).Once() client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("InspectContainer", "test").Return(&dockerclient.ContainerInfo{Config: &dockerclient.ContainerConfig{CpuShares: cpuShares}}, nil).Once() engine.ConnectWithClient(client) @@ -317,6 +324,7 @@ func TestContainerRemovedDuringRefresh(t *testing.T) { client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return() client.On("ListContainers", true, false, "").Return([]dockerclient.Container{container1, container2}, nil) client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("InspectContainer", "c1").Return(info1, errors.New("Not found")) client.On("InspectContainer", "c2").Return(info2, nil) diff --git a/cluster/mesos/cluster.go b/cluster/mesos/cluster.go index 424729d19b..daaae11f4b 100644 --- a/cluster/mesos/cluster.go +++ b/cluster/mesos/cluster.go @@ -226,11 +226,21 @@ func (c *Cluster) RemoveImages(name string, force bool) ([]*dockerclient.ImageDe return nil, errNotSupported } +// CreateNetwork creates a network in the cluster +func (c *Cluster) CreateNetwork(request *dockerclient.NetworkCreate) (*dockerclient.NetworkCreateResponse, error) { + return nil, errNotSupported +} + // CreateVolume creates a volume in the cluster func (c *Cluster) CreateVolume(request *dockerclient.VolumeCreateRequest) (*cluster.Volume, error) { return nil, errNotSupported } +// RemoveNetwork removes network from the cluster +func (c *Cluster) RemoveNetwork(network *cluster.Network) error { + return errNotSupported +} + // RemoveVolumes removes volumes from the cluster func (c *Cluster) RemoveVolumes(name string) (bool, error) { return false, errNotSupported @@ -306,6 +316,11 @@ func (c *Cluster) RenameContainer(container *cluster.Container, newName string) return nil } +// Networks returns all the networks in the cluster. +func (c *Cluster) Networks() cluster.Networks { + return cluster.Networks{} +} + // Volumes returns all the volumes in the cluster. func (c *Cluster) Volumes() []*cluster.Volume { return nil diff --git a/cluster/network.go b/cluster/network.go new file mode 100644 index 0000000000..9cede7a2e4 --- /dev/null +++ b/cluster/network.go @@ -0,0 +1,69 @@ +package cluster + +import ( + "strings" + + "github.com/docker/docker/pkg/stringid" + "github.com/samalba/dockerclient" +) + +// Network is exported +type Network struct { + dockerclient.NetworkResource + + Engine *Engine +} + +// Networks represents a map of networks +type Networks []*Network + +// Get returns a network using it's ID or Name +func (networks Networks) Get(IDOrName string) *Network { + // Abort immediately if the name is empty. + if len(IDOrName) == 0 { + return nil + } + + // Match exact or short Network ID. + for _, network := range networks { + if network.ID == IDOrName || stringid.TruncateID(network.ID) == IDOrName { + return network + } + } + + candidates := []*Network{} + + // Match name, /name or engine/name. + for _, network := range networks { + if network.Name == IDOrName || network.Engine.ID+"/"+network.Name == IDOrName || network.Engine.Name+"/"+network.Name == IDOrName { + candidates = append(candidates, network) + } + } + + if size := len(candidates); size == 1 { + return candidates[0] + } else if size > 1 { + return nil + } + + // Match name, /name or engine/name. + for _, network := range networks { + if network.Name == "/"+IDOrName { + return network + } + } + + // Match Network ID prefix. + for _, network := range networks { + if strings.HasPrefix(network.ID, IDOrName) { + candidates = append(candidates, network) + } + } + + if len(candidates) == 1 { + return candidates[0] + } + + return nil + +} diff --git a/cluster/swarm/cluster.go b/cluster/swarm/cluster.go index fa6196f635..4157ca3c68 100644 --- a/cluster/swarm/cluster.go +++ b/cluster/swarm/cluster.go @@ -174,6 +174,11 @@ func (c *Cluster) RemoveContainer(container *cluster.Container, force, volumes b return container.Engine.RemoveContainer(container, force, volumes) } +// RemoveNetwork removes a network from the cluster +func (c *Cluster) RemoveNetwork(network *cluster.Network) error { + return network.Engine.RemoveNetwork(network) +} + func (c *Cluster) getEngineByAddr(addr string) *cluster.Engine { c.RLock() defer c.RUnlock() @@ -330,6 +335,29 @@ func (c *Cluster) RemoveImages(name string, force bool) ([]*dockerclient.ImageDe return out, err } +// CreateNetwork creates a network in the cluster +func (c *Cluster) CreateNetwork(request *dockerclient.NetworkCreate) (response *dockerclient.NetworkCreateResponse, err error) { + var ( + parts = strings.SplitN(request.Name, "/", 2) + config = &cluster.ContainerConfig{} + ) + + if len(parts) == 2 { + // a node was specified, create the container only on this node + request.Name = parts[1] + config = cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==" + parts[0]}}) + } + + n, err := c.scheduler.SelectNodeForContainer(c.listNodes(), config) + if err != nil { + return nil, err + } + if n != nil { + return c.engines[n.ID].CreateNetwork(request) + } + return nil, nil +} + // CreateVolume creates a volume in the cluster func (c *Cluster) CreateVolume(request *dockerclient.VolumeCreateRequest) (*cluster.Volume, error) { var ( @@ -570,8 +598,20 @@ func (c *Cluster) Container(IDOrName string) *cluster.Container { c.RLock() defer c.RUnlock() - return cluster.Containers(c.Containers()).Get(IDOrName) + return c.Containers().Get(IDOrName) +} + +// Networks returns all the networks in the cluster. +func (c *Cluster) Networks() cluster.Networks { + c.RLock() + defer c.RUnlock() + + out := cluster.Networks{} + for _, e := range c.engines { + out = append(out, e.Networks()...) + } + return out } // Volumes returns all the volumes in the cluster. diff --git a/cluster/swarm/cluster_test.go b/cluster/swarm/cluster_test.go index 0efa96fedb..f99e9d4467 100644 --- a/cluster/swarm/cluster_test.go +++ b/cluster/swarm/cluster_test.go @@ -131,6 +131,7 @@ func TestImportImage(t *testing.T) { client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil) client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) // connect client engine.ConnectWithClient(client) @@ -180,6 +181,7 @@ func TestLoadImage(t *testing.T) { client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil) client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) // connect client engine.ConnectWithClient(client) @@ -232,6 +234,7 @@ func TestTagImage(t *testing.T) { client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once() client.On("ListImages", mock.Anything).Return(images, nil) client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) + client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) // connect client engine.ConnectWithClient(client) diff --git a/docs/networking.md b/docs/networking.md new file mode 100644 index 0000000000..242bca1ca7 --- /dev/null +++ b/docs/networking.md @@ -0,0 +1,106 @@ + + +# Networking + +Docker Swarm is fully compatible for the new networking model added in docker 1.9 + +## Setup + +To use multi-host networking you need to start your docker engines with +`--cluster-store` and `--cluster-advertise` as indicated in the docker +engine docs. + +### List networks + +This example assumes there are two nodes `node-0` and `node-1` in the cluster. + + $ docker networks ls + NETWORK ID NAME DRIVER + 3dd50db9706d node-0/host host + 09138343e80e node-0/bridge bridge + 8834dbd552e5 node-0/none null + 45782acfe427 node-1/host host + 8926accb25fd node-1/bridge bridge + 6382abccd23d node-1/none null + +As you can see, each network name is prefixed by the node name. + +## Create a network + +By default, swarm is using the `overlay` network driver, a global +scope driver. + + $ docker network create swarm_network + 42131321acab3233ba342443Ba4312 + $ docker networks ls + NETWORK ID NAME DRIVER + 3dd50db9706d node-0/host host + 09138343e80e node-0/bridge bridge + 8834dbd552e5 node-0/none null + 42131321acab node-0/swarm_network overlay + 45782acfe427 node-1/host host + 8926accb25fd node-1/bridge bridge + 6382abccd23d node-1/none null + 42131321acab node-1/swarm_network overlay + +As you can see here, the ID is the same on the two nodes, because it's the same +network. + +If you want to want to create a local scope network (for example with the bridge +driver) you should use `/` otherwise your network will be created on a +random node. + + $ docker network create node-0/bridge2 -b bridge + 921817fefea521673217123abab223 + $ docker network create node-1/bridge2 -b bridge + 5262bbfe5616fef6627771289aacc2 + $ docker networks ls + NETWORK ID NAME DRIVER + 3dd50db9706d node-0/host host + 09138343e80e node-0/bridge bridge + 8834dbd552e5 node-0/none null + 42131321acab node-0/swarm_network overlay + 921817fefea5 node-0/bridge2 brige + 45782acfe427 node-1/host host + 8926accb25fd node-1/bridge bridge + 6382abccd23d node-1/none null + 42131321acab node-1/swarm_network overlay + 5262bbfe5616 node-1/bridge2 bridge + +## Remove a network + +To remove a network you can use its ID or its name. +If two different network have the same name, use may use `/`. + + $ docker network rm swarm_network + 42131321acab3233ba342443Ba4312 + $ docker network rm node-0/bridge2 + 921817fefea521673217123abab223 + $ docker networks ls + NETWORK ID NAME DRIVER + 3dd50db9706d node-0/host host + 09138343e80e node-0/bridge bridge + 8834dbd552e5 node-0/none null + 45782acfe427 node-1/host host + 8926accb25fd node-1/bridge bridge + 6382abccd23d node-1/none null + 5262bbfe5616 node-1/bridge2 bridge + +`swarm_network` was removed from every node, `bridge2` was removed only +from `node-0`. + +## Docker Swarm documentation index + +- [User guide](/) +- [Scheduler strategies](/scheduler/strategy.md) +- [Scheduler filters](/scheduler/filter.md) +- [Swarm API](/api/swarm-api.md) diff --git a/test/integration/api/network.bats b/test/integration/api/network.bats new file mode 100644 index 0000000000..d5c52ce1fa --- /dev/null +++ b/test/integration/api/network.bats @@ -0,0 +1,94 @@ +#!/usr/bin/env bats + +load ../helpers + +function teardown() { + swarm_manage_cleanup + stop_docker +} + +@test "docker network ls" { + start_docker 2 + swarm_manage + + run docker_swarm network ls + [ "${#lines[@]}" -eq 7 ] +} + +@test "docker network inspect" { + start_docker_with_busybox 2 + swarm_manage + + # run + docker_swarm run -d -e constraint:node==node-0 busybox sleep 100 + + run docker_swarm network inspect bridge + [ "$status" -ne 0 ] + + run docker_swarm network inspect node-0/bridge + [ "${#lines[@]}" -eq 13 ] +} + +@test "docker network create" { + start_docker 2 + swarm_manage + + run docker_swarm network ls + [ "${#lines[@]}" -eq 7 ] + + docker_swarm network create -d bridge test1 + run docker_swarm network ls + [ "${#lines[@]}" -eq 8 ] + + docker_swarm network create -d bridge node-1/test2 + run docker_swarm network ls + [ "${#lines[@]}" -eq 9 ] + + run docker_swarm network create -d bridge node-2/test3 + [ "$status" -ne 0 ] +} + +@test "docker network rm" { + start_docker_with_busybox 2 + swarm_manage + + run docker_swarm network rm test_network + [ "$status" -ne 0 ] + + run docker_swarm network rm bridge + [ "$status" -ne 0 ] + + docker_swarm network create -d bridge node-0/test + run docker_swarm network ls + [ "${#lines[@]}" -eq 8 ] + + docker_swarm network rm node-0/test + run docker_swarm network ls + [ "${#lines[@]}" -eq 7 ] +} + +@test "docker network disconnect connect" { + start_docker_with_busybox 2 + swarm_manage + + # run + docker_swarm run -d --name test_container -e constraint:node==node-0 busybox sleep 100 + + run docker_swarm network inspect node-0/bridge + [ "${#lines[@]}" -eq 13 ] + + docker_swarm network disconnect node-0/bridge test_container + + run docker_swarm network inspect node-0/bridge + [ "${#lines[@]}" -eq 6 ] + + docker_swarm network connect node-0/bridge test_container + + run docker_swarm network inspect node-0/bridge + [ "${#lines[@]}" -eq 13 ] + + docker_swarm rm -f test_container + + run docker_swarm network inspect node-0/bridge + [ "${#lines[@]}" -eq 6 ] +} diff --git a/test/integration/store.bats b/test/integration/store.bats new file mode 100644 index 0000000000..e144195e01 --- /dev/null +++ b/test/integration/store.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats + +load helpers + +function teardown() { + swarm_manage_cleanup + stop_docker + stop_store +} + +# Address on which the store will listen (random port between 8000 and 9000). +STORE_HOST=127.0.0.1:$(( ( RANDOM % 1000 ) + 8000 )) + +# Discovery parameter for Swarm +DISCOVERY="consul://${STORE_HOST}/test" + +# Container name for integration test +CONTAINER_NAME=swarm_consul + +function start_store() { + docker_host run -v $(pwd)/discovery/consul/config:/config --name=$CONTAINER_NAME -h $CONTAINER_NAME -p $STORE_HOST:8500 -d progrium/consul -server -bootstrap-expect 1 -config-file=/config/consul.json +} + +function stop_store() { + docker_host rm -f -v $CONTAINER_NAME || true +} + + +@test "docker setup cluster-store" { + start_store + + start_docker_with_busybox 1 --cluster-store $DISCOVERY --cluster-advertise $HOSTS[0] + start_docker_with_busybox 1 --cluster-store $DISCOVERY --cluster-advertise $HOSTS[1] + + run docker_host -H ${HOSTS[0]} info + [ "$status" -eq 0 ] + [[ "${output}" == *"$DISCOVERY"* ]] + + run docker_host -H ${HOSTS[1]} info + [ "$status" -eq 0 ] + [[ "${output}" == *"$DISCOVERY"* ]] + + swarm_manage + run docker_swarm info + [ "$status" -eq 0 ] + [[ "${output}" == *"Nodes: 2"* ]] +}