diff --git a/api/client/service.go b/api/client/service.go new file mode 100644 index 0000000000..d7308449e0 --- /dev/null +++ b/api/client/service.go @@ -0,0 +1,15 @@ +// +build experimental + +package client + +import ( + "os" + + nwclient "github.com/docker/libnetwork/client" +) + +func (cli *DockerCli) CmdService(args ...string) error { + nCli := nwclient.NewNetworkCli(cli.out, cli.err, nwclient.CallFunc(cli.call)) + args = append([]string{"service"}, args...) + return nCli.Cmd(os.Args[0], args...) +} diff --git a/api/server/server_experimental.go b/api/server/server_experimental.go index a93b58af1e..06f55013ed 100644 --- a/api/server/server_experimental.go +++ b/api/server/server_experimental.go @@ -9,4 +9,9 @@ func (s *Server) registerSubRouter() { subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler) subrouter = s.router.PathPrefix("/networks").Subrouter() subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler) + + subrouter = s.router.PathPrefix("/v{version:[0-9.]+}/services").Subrouter() + subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler) + subrouter = s.router.PathPrefix("/services").Subrouter() + subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler) } diff --git a/daemon/container_linux.go b/daemon/container_linux.go index 9c8945499e..e8f918a048 100644 --- a/daemon/container_linux.go +++ b/daemon/container_linux.go @@ -737,11 +737,23 @@ func (container *Container) buildCreateEndpointOptions() ([]libnetwork.EndpointO return createOptions, nil } -func createDefaultNetwork(controller libnetwork.NetworkController) (libnetwork.Network, error) { +func parseService(controller libnetwork.NetworkController, service string) (string, string, string) { + dn := controller.Config().Daemon.DefaultNetwork + dd := controller.Config().Daemon.DefaultDriver + + snd := strings.Split(service, ".") + if len(snd) > 2 { + return strings.Join(snd[:len(snd)-2], "."), snd[len(snd)-2], snd[len(snd)-1] + } + if len(snd) > 1 { + return snd[0], snd[1], dd + } + return snd[0], dn, dd +} + +func createNetwork(controller libnetwork.NetworkController, dnet string, driver string) (libnetwork.Network, error) { createOptions := []libnetwork.NetworkOption{} genericOption := options.Generic{} - dnet := controller.Config().Daemon.DefaultNetwork - driver := controller.Config().Daemon.DefaultDriver // Bridge driver is special due to legacy reasons if runconfig.NetworkMode(driver).IsBridge() { @@ -763,31 +775,53 @@ func (container *Container) AllocateNetwork() error { return nil } + var networkDriver string + service := container.Config.PublishService networkName := mode.NetworkName() if mode.IsDefault() { - networkName = controller.Config().Daemon.DefaultNetwork + if service != "" { + service, networkName, networkDriver = parseService(controller, service) + } else { + networkName = controller.Config().Daemon.DefaultNetwork + networkDriver = controller.Config().Daemon.DefaultDriver + } + } else if service != "" { + return fmt.Errorf("conflicting options: publishing a service and network mode") + } + + if service == "" { + service = strings.Replace(container.Name, ".", "-", -1) } var err error n, err := controller.NetworkByName(networkName) if err != nil { - if !mode.IsDefault() { - return fmt.Errorf("error locating network with name %s: %v", networkName, err) + // Create Network automatically only in default mode + if _, ok := err.(libnetwork.ErrNoSuchNetwork); !ok || !mode.IsDefault() { + return err } - if n, err = createDefaultNetwork(controller); err != nil { + + if n, err = createNetwork(controller, networkName, networkDriver); err != nil { return err } } - createOptions, err := container.buildCreateEndpointOptions() + ep, err := n.EndpointByName(service) if err != nil { - return err - } + if _, ok := err.(libnetwork.ErrNoSuchEndpoint); !ok { + return err + } - ep, err := n.CreateEndpoint(container.Name, createOptions...) - if err != nil { - return err + createOptions, err := container.buildCreateEndpointOptions() + if err != nil { + return err + } + + ep, err = n.CreateEndpoint(service, createOptions...) + if err != nil { + return err + } } if err := container.updateNetworkSettings(n, ep); err != nil { diff --git a/experimental/networking.md b/experimental/networking.md index f3bd66b615..15372325f1 100644 --- a/experimental/networking.md +++ b/experimental/networking.md @@ -2,7 +2,10 @@ In this feature: -- `network` become a first class objects in the Docker UI +- `network` and `service` become a first class objects in the Docker UI +- You can create networks and attach containers to them +- We introduce the concept of `services` + - This is an entry-point in to a given network that is also published via Service Discovery This is an experimental feature. For information on installing and using experimental features, see [the experimental feature overview](experimental.md). @@ -59,14 +62,53 @@ If you no longer have need of a network, you can delete it with `docker network bd61375b6993 host host cc455abccfeb bridge bridge - -Currently the only way this network can be used to connect container is via default network-mode. Docker daemon supports a configuration flag `--default-network` which takes configuration value of format `NETWORK:DRIVER`, where, `NETWORK` is the name of the network created using the `docker network create` command and `DRIVER` represents the in-built drivers such as bridge, overlay, container, host and none. or Remote drivers via Network Plugins. When a container is created and if the network mode (`--net`) is not specified, then this default network will be used to connect the container. If `--default-network` is not specified, the default network will be the `bridge` driver. +## Using Services + + Usage: docker service COMMAND [OPTIONS] [arg...] + + Commands: + publish Publish a service + unpublish Remove a service + attach Attach a backend (container) to the service + detach Detach the backend from the service + ls Lists all services + info Display information about a service + + Run 'docker service COMMAND --help' for more information on a command. + + --help=false Print usage + +Assuming we want to publish a service from container `a0ebc12d3e48` on network `foo` as `my-service` we would use the following command: + + $ docker service publish my-service.foo + ec56fd74717d00f968c26675c9a77707e49ae64b8e54832ebf78888eb116e428 + $ docker service attach a0ebc12d3e48 my-service.foo + +This would make the container `a0ebc12d3e48` accessible as `my-service` on network `foo`. Any other container in network `foo` can use DNS to resolve the address of `my-service` + +This can also be acheived by using the `--publish-service` flag for `docker run`: + + docker run -itd --publish-service db.foo postgres + +`db.foo` in this instance means "place the container on network `foo`, and allow other hosts on `foo` to discover it under the name `db`" + +We can see the current services using the `docker service ls` command + + $ docker service ls + SERVICE ID NAME NETWORK PROVIDER + ec56fd74717d my-service foo a0ebc12d3e48 + +To remove the a service: + + $ docker service detach a0ebc12d3e48 my-service.foo + $ docker service unpublish my-service.foo + Send us feedback and comments on [#](https://github.com/docker/docker/issues/?), or on the usual Google Groups (docker-user, docker-dev) and IRC channels. diff --git a/experimental/networking_api.md b/experimental/networking_api.md index 7af86b77d0..829c158718 100644 --- a/experimental/networking_api.md +++ b/experimental/networking_api.md @@ -285,3 +285,205 @@ Status Codes: - **200** – no error - **404** – not found - **500** – server error + +# Services API + +### Publish a Service + +`POST /services` + +Publish a service + +**Example Request** + + POST /services HTTP/1.1 + Content-Type: application/json + + { + "name": "bar", + "network_name": "foo", + "exposed_ports": null, + "port_mapping": null + } + +**Example Response** + + HTTP/1.1 200 OK + Content-Type: application/json + + "0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff" + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Get a Service + +`GET /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff` + +Get a service + +**Example Request**: + + GET /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff HTTP/1.1 + +**Example Response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "name": "bar", + "id": "0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff", + "network": "foo" + } + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** - not found +- **500** – server error + +### Attach a backend to a service + +`POST /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff/backend` + +Attach a backend to a service + +**Example Request**: + + POST /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff/backend HTTP/1.1 + Content-Type: application/json + + { + "container_id": "98c5241f9475e9efc17e7198e931fb48166010b80f96d48df204e251378ca547", + "host_name": "", + "domain_name": "", + "hosts_path": "", + "resolv_conf_path": "", + "dns": null, + "extra_hosts": null, + "parent_updates": null, + "use_default_sandbox": false + } + +**Example Response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + "/var/run/docker/netns/98c5241f9475" + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Get Backends for a Service + +Get all backends for a given service + +**Example Request** + + GET /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff/backend HTTP/1.1 + +**Example Response** + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "id": "98c5241f9475e9efc17e7198e931fb48166010b80f96d48df204e251378ca547" + } + ] + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### List Services + +`GET /services` + +List services + +**Example request**: + + GET /services HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "name": "/stupefied_stallman", + "id": "c826b26bf736fb4a77db33f83562e59f9a770724e259ab9c3d50d948f8233ae4", + "network": "bridge" + }, + { + "name": "bar", + "id": "0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff", + "network": "foo" + } + ] + +Query Parameters: + +- **name** – Filter results with the given name +- **partial-id** – Filter results using the partial network ID +- **network** - Filter results by the given network + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Detach a Backend from a Service + +`DELETE /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff/backend/98c5241f9475e9efc17e7198e931fb48166010b80f96d48df204e251378ca547` + +Detach a backend from a service + +**Example Request** + + DELETE /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff/backend/98c5241f9475e9efc17e7198e931fb48166010b80f96d48df204e251378ca547 HTTP/1.1 + +**Example Response** + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Un-Publish a Service + +`DELETE /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff` + +Unpublish a service + +**Example Request** + + DELETE /services/0aee0899e6c5e903cf3ef2bdc28a1c9aaf639c8c8c331fa4ae26344d9e32c1ff HTTP/1.1 + +**Example Response** + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error diff --git a/integration-cli/docker_api_service_test.go b/integration-cli/docker_api_service_test.go new file mode 100644 index 0000000000..df072197c7 --- /dev/null +++ b/integration-cli/docker_api_service_test.go @@ -0,0 +1,113 @@ +// +build experimental + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/go-check/check" +) + +func isServiceAvailable(c *check.C, name string, network string) bool { + status, body, err := sockRequest("GET", "/services", nil) + c.Assert(status, check.Equals, http.StatusOK) + c.Assert(err, check.IsNil) + + var inspectJSON []struct { + Name string + ID string + Network string + } + if err = json.Unmarshal(body, &inspectJSON); err != nil { + c.Fatalf("unable to unmarshal response body: %v", err) + } + for _, s := range inspectJSON { + if s.Name == name && s.Network == network { + return true + } + } + return false + +} + +func isServiceNetworkAvailable(c *check.C, name string) bool { + status, body, err := sockRequest("GET", "/networks", nil) + c.Assert(status, check.Equals, http.StatusOK) + c.Assert(err, check.IsNil) + + var inspectJSON []struct { + Name string + ID string + Type string + } + if err = json.Unmarshal(body, &inspectJSON); err != nil { + c.Fatalf("unable to unmarshal response body: %v", err) + } + for _, n := range inspectJSON { + if n.Name == name { + return true + } + } + return false + +} + +func (s *DockerSuite) TestServiceApiCreateDelete(c *check.C) { + name := "testnetwork" + config := map[string]interface{}{ + "name": name, + "network_type": "bridge", + } + + status, resp, err := sockRequest("POST", "/networks", config) + c.Assert(status, check.Equals, http.StatusCreated) + c.Assert(err, check.IsNil) + + if !isServiceNetworkAvailable(c, name) { + c.Fatalf("Network %s not found", name) + } + + var nid string + err = json.Unmarshal(resp, &nid) + if err != nil { + c.Fatal(err) + } + + sname := "service1" + sconfig := map[string]interface{}{ + "name": sname, + "network_name": name, + } + + status, resp, err = sockRequest("POST", "/services", sconfig) + c.Assert(status, check.Equals, http.StatusCreated) + c.Assert(err, check.IsNil) + + if !isServiceAvailable(c, sname, name) { + c.Fatalf("Service %s.%s not found", sname, name) + } + + var id string + err = json.Unmarshal(resp, &id) + if err != nil { + c.Fatal(err) + } + + status, _, err = sockRequest("DELETE", fmt.Sprintf("/services/%s", id), nil) + c.Assert(status, check.Equals, http.StatusOK) + c.Assert(err, check.IsNil) + + if isServiceAvailable(c, sname, name) { + c.Fatalf("Service %s.%s not deleted", sname, name) + } + + status, _, err = sockRequest("DELETE", fmt.Sprintf("/networks/%s", nid), nil) + c.Assert(status, check.Equals, http.StatusOK) + c.Assert(err, check.IsNil) + + if isNetworkAvailable(c, name) { + c.Fatalf("Network %s not deleted", name) + } +} diff --git a/integration-cli/docker_cli_network_test.go b/integration-cli/docker_cli_network_test.go index 820a0ae8a9..a79d65a92e 100644 --- a/integration-cli/docker_cli_network_test.go +++ b/integration-cli/docker_cli_network_test.go @@ -9,12 +9,22 @@ import ( "github.com/go-check/check" ) -func isNetworkPresent(c *check.C, name string) bool { +func assertNwIsAvailable(c *check.C, name string) { + if !isNwPresent(c, name) { + c.Fatalf("Network %s not found in network ls o/p", name) + } +} + +func assertNwNotAvailable(c *check.C, name string) { + if isNwPresent(c, name) { + c.Fatalf("Found network %s in network ls o/p", name) + } +} + +func isNwPresent(c *check.C, name string) bool { runCmd := exec.Command(dockerBinary, "network", "ls") out, _, _, err := runCommandWithStdoutStderr(runCmd) - if err != nil { - c.Fatal(out, err) - } + c.Assert(err, check.IsNil) lines := strings.Split(out, "\n") for i := 1; i < len(lines)-1; i++ { if strings.Contains(lines[i], name) { @@ -27,28 +37,18 @@ func isNetworkPresent(c *check.C, name string) bool { func (s *DockerSuite) TestDockerNetworkLsDefault(c *check.C) { defaults := []string{"bridge", "host", "none"} for _, nn := range defaults { - if !isNetworkPresent(c, nn) { - c.Fatalf("Missing Default network : %s", nn) - } + assertNwIsAvailable(c, nn) } } func (s *DockerSuite) TestDockerNetworkCreateDelete(c *check.C) { runCmd := exec.Command(dockerBinary, "network", "create", "test") - out, _, _, err := runCommandWithStdoutStderr(runCmd) - if err != nil { - c.Fatal(out, err) - } - if !isNetworkPresent(c, "test") { - c.Fatalf("Network test not found") - } + _, _, _, err := runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + assertNwIsAvailable(c, "test") runCmd = exec.Command(dockerBinary, "network", "rm", "test") - out, _, _, err = runCommandWithStdoutStderr(runCmd) - if err != nil { - c.Fatal(out, err) - } - if isNetworkPresent(c, "test") { - c.Fatalf("Network test is not removed") - } + _, _, _, err = runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + assertNwNotAvailable(c, "test") } diff --git a/integration-cli/docker_cli_service_test.go b/integration-cli/docker_cli_service_test.go new file mode 100644 index 0000000000..d61871e1bd --- /dev/null +++ b/integration-cli/docker_cli_service_test.go @@ -0,0 +1,86 @@ +// +build experimental + +package main + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/go-check/check" +) + +func assertSrvIsAvailable(c *check.C, sname, name string) { + if !isSrvPresent(c, sname, name) { + c.Fatalf("Service %s on network %s not found in service ls o/p", sname, name) + } +} + +func assertSrvNotAvailable(c *check.C, sname, name string) { + if isSrvPresent(c, sname, name) { + c.Fatalf("Found service %s on network %s in service ls o/p", sname, name) + } +} + +func isSrvPresent(c *check.C, sname, name string) bool { + runCmd := exec.Command(dockerBinary, "service", "ls") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + lines := strings.Split(out, "\n") + for i := 1; i < len(lines)-1; i++ { + if strings.Contains(lines[i], sname) && strings.Contains(lines[i], name) { + return true + } + } + return false +} + +func isCntPresent(c *check.C, cname, sname, name string) bool { + runCmd := exec.Command(dockerBinary, "service", "ls", "--no-trunc") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + lines := strings.Split(out, "\n") + for i := 1; i < len(lines)-1; i++ { + fmt.Println(lines) + if strings.Contains(lines[i], name) && strings.Contains(lines[i], sname) && strings.Contains(lines[i], cname) { + return true + } + } + return false +} + +func (s *DockerSuite) TestDockerServiceCreateDelete(c *check.C) { + runCmd := exec.Command(dockerBinary, "network", "create", "test") + _, _, _, err := runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + assertNwIsAvailable(c, "test") + + runCmd = exec.Command(dockerBinary, "service", "publish", "s1.test") + _, _, _, err = runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + assertSrvIsAvailable(c, "s1", "test") + + runCmd = exec.Command(dockerBinary, "service", "unpublish", "s1.test") + _, _, _, err = runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + assertSrvNotAvailable(c, "s1", "test") + + runCmd = exec.Command(dockerBinary, "network", "rm", "test") + _, _, _, err = runCommandWithStdoutStderr(runCmd) + c.Assert(err, check.IsNil) + assertNwNotAvailable(c, "test") +} + +func (s *DockerSuite) TestDockerPublishServiceFlag(c *check.C) { + // Run saying the container is the backend for the specified service on the specified network + runCmd := exec.Command(dockerBinary, "run", "-d", "--expose=23", "--publish-service", "telnet.production", "busybox", "top") + out, _, err := runCommandWithOutput(runCmd) + c.Assert(err, check.IsNil) + cid := strings.TrimSpace(out) + + // Verify container is attached in service ps o/p + assertSrvIsAvailable(c, "telnet", "production") + runCmd = exec.Command(dockerBinary, "rm", "-f", cid) + out, _, err = runCommandWithOutput(runCmd) + c.Assert(err, check.IsNil) +} diff --git a/runconfig/config.go b/runconfig/config.go index 8c578ee6cc..786b075618 100644 --- a/runconfig/config.go +++ b/runconfig/config.go @@ -114,6 +114,7 @@ type Config struct { AttachStdout bool AttachStderr bool ExposedPorts map[nat.Port]struct{} + PublishService string Tty bool // Attach standard streams to a tty, including stdin if it is not closed. OpenStdin bool // Open stdin StdinOnce bool // If true, close stdin after the 1 attached client disconnects. diff --git a/runconfig/parse_experimental.go b/runconfig/parse_experimental.go index 886b377fa8..d00e69c5c6 100644 --- a/runconfig/parse_experimental.go +++ b/runconfig/parse_experimental.go @@ -11,9 +11,11 @@ type experimentalFlags struct { func attachExperimentalFlags(cmd *flag.FlagSet) *experimentalFlags { flags := make(map[string]interface{}) flags["volume-driver"] = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") + flags["publish-service"] = cmd.String([]string{"-publish-service"}, "", "Publish this container as a service") return &experimentalFlags{flags: flags} } func applyExperimentalFlags(exp *experimentalFlags, config *Config, hostConfig *HostConfig) { config.VolumeDriver = *(exp.flags["volume-driver"]).(*string) + config.PublishService = *(exp.flags["publish-service"]).(*string) }