diff --git a/.gitignore b/.gitignore index 94b5d700..6f39f896 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,9 @@ coverage.html # Project specific weaver/weaver weavedns/weavedns +weaveproxy/weaveproxy sigproxy/sigproxy +weavewait/weavewait tools/bin tools/build releases @@ -40,6 +42,8 @@ weave.tar weavedns.tar weaveexec.tar weaveexec/weave +weaveexec/weaveproxy +weaveexec/weavewait weaveexec/sigproxy weaveexec/docker*.tgz Vagrantfile.local diff --git a/Makefile b/Makefile index 601e6430..463ba6cd 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,9 @@ DOCKERHUB_USER=weaveworks WEAVE_VERSION=git-$(shell git rev-parse --short=12 HEAD) WEAVER_EXE=weaver/weaver WEAVEDNS_EXE=weavedns/weavedns +WEAVEPROXY_EXE=weaveproxy/weaveproxy SIGPROXY_EXE=sigproxy/sigproxy +WEAVEWAIT_EXE=weavewait/weavewait WEAVER_IMAGE=$(DOCKERHUB_USER)/weave WEAVEDNS_IMAGE=$(DOCKERHUB_USER)/weavedns WEAVEEXEC_IMAGE=$(DOCKERHUB_USER)/weaveexec @@ -27,9 +29,9 @@ all: $(WEAVER_EXPORT) $(WEAVEDNS_EXPORT) $(WEAVEEXEC_EXPORT) travis: $(WEAVER_EXE) $(WEAVEDNS_EXE) update: - go get -u -f -v -tags -netgo ./$(dir $(WEAVER_EXE)) ./$(dir $(WEAVEDNS_EXE)) ./$(dir $(SIGPROXY_EXE)) + go get -u -f -v -tags -netgo ./$(dir $(WEAVER_EXE)) ./$(dir $(WEAVEDNS_EXE)) ./$(dir $(SIGPROXY_EXE)) ./$(dir $(WEAVEPROXY_EXE)) -$(WEAVER_EXE) $(WEAVEDNS_EXE): common/*.go +$(WEAVER_EXE) $(WEAVEDNS_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE): common/*.go go get -tags netgo ./$(@D) go build -ldflags "-extldflags \"-static\" -X main.version $(WEAVE_VERSION)" -tags netgo -o $@ ./$(@D) @strings $@ | grep cgo_stub\\\.go >/dev/null || { \ @@ -43,6 +45,8 @@ $(WEAVER_EXE) $(WEAVEDNS_EXE): common/*.go $(WEAVER_EXE): router/*.go ipam/*.go ipam/*/*.go weaver/main.go $(WEAVEDNS_EXE): nameserver/*.go weavedns/main.go +$(WEAVEPROXY_EXE): proxy/*.go weaveproxy/main.go +$(WEAVEWAIT_EXE): weavewait/*.go weavewait/main.go # Sigproxy needs separate rule as it fails the netgo check in the main # build stanza due to not importing net package @@ -57,9 +61,11 @@ $(WEAVEDNS_EXPORT): weavedns/Dockerfile $(WEAVEDNS_EXE) $(SUDO) docker build -t $(WEAVEDNS_IMAGE) weavedns $(SUDO) docker save $(WEAVEDNS_IMAGE):latest > $@ -$(WEAVEEXEC_EXPORT): weaveexec/Dockerfile $(DOCKER_DISTRIB) weave $(SIGPROXY_EXE) +$(WEAVEEXEC_EXPORT): weaveexec/Dockerfile $(DOCKER_DISTRIB) weave $(SIGPROXY_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE) cp weave weaveexec/weave - cp sigproxy/sigproxy weaveexec/sigproxy + cp $(SIGPROXY_EXE) weaveexec/sigproxy + cp $(WEAVEWAIT_EXE) weaveexec/weavewait + cp $(WEAVEPROXY_EXE) weaveexec/weaveproxy cp $(DOCKER_DISTRIB) weaveexec/docker.tgz $(SUDO) docker build -t $(WEAVEEXEC_IMAGE) weaveexec $(SUDO) docker save $(WEAVEEXEC_IMAGE):latest > $@ @@ -92,7 +98,7 @@ publish: $(PUBLISH) clean: -$(SUDO) docker rmi $(WEAVER_IMAGE) $(WEAVEDNS_IMAGE) $(WEAVEEXEC_IMAGE) - rm -f $(WEAVER_EXE) $(WEAVEDNS_EXE) $(SIGPROXY_EXE) $(WEAVER_EXPORT) $(WEAVEDNS_EXPORT) $(WEAVEEXEC_EXPORT) + rm -f $(WEAVER_EXE) $(WEAVEDNS_EXE) $(SIGPROXY_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE) $(WEAVER_EXPORT) $(WEAVEDNS_EXPORT) $(WEAVEEXEC_EXPORT) build: $(SUDO) go clean -i net diff --git a/proxy/client.go b/proxy/client.go new file mode 100644 index 00000000..f0733262 --- /dev/null +++ b/proxy/client.go @@ -0,0 +1,167 @@ +package proxy + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + + "github.com/fsouza/go-dockerclient" + . "github.com/weaveworks/weave/common" +) + +type client struct { + interceptor + Dial func() (net.Conn, error) +} + +func newClient(dial func() (net.Conn, error), i interceptor) *client { + return &client{ + interceptor: i, + Dial: dial, + } +} + +func (c *client) ServeHTTP(w http.ResponseWriter, r *http.Request) { + err := c.InterceptRequest(r) + if err == docker.ErrNoSuchImage { + http.NotFound(w, r) + return + } + if err != nil { + http.Error(w, "Unable to intercept request", http.StatusInternalServerError) + Warning.Print("Error intercepting request: ", err) + return + } + + conn, err := c.Dial() + if err != nil { + http.Error(w, "Could not connect to target", http.StatusInternalServerError) + Warning.Print(err) + return + } + client := httputil.NewClientConn(conn, nil) + defer client.Close() + + resp, err := client.Do(r) + if err != nil && err != httputil.ErrPersistEOF { + http.Error(w, fmt.Sprintf("Could not make request to target: %v", err), http.StatusInternalServerError) + Warning.Print("Error forwarding request: ", err) + return + } + err = c.InterceptResponse(resp) + if err != nil { + http.Error(w, "Unable to intercept response", http.StatusInternalServerError) + Warning.Print("Error intercepting response: ", err) + return + } + + hdr := w.Header() + for k, vs := range resp.Header { + for _, v := range vs { + hdr.Add(k, v) + } + } + Debug.Printf("Response from target: %s %v", resp.Status, w.Header()) + + if resp.Header.Get("Content-Type") == "application/vnd.docker.raw-stream" { + doRawStream(w, resp, client) + } else if resp.TransferEncoding != nil && resp.TransferEncoding[0] == "chunked" { + doChunkedResponse(w, resp, client) + } else { + w.WriteHeader(resp.StatusCode) + if _, err := io.Copy(w, resp.Body); err != nil { + Warning.Print(err) + } + } +} + +func doRawStream(w http.ResponseWriter, resp *http.Response, client *httputil.ClientConn) { + down, downBuf, up, rem, err := hijack(w, client) + if err != nil { + http.Error(w, "Unable to hijack connection for raw stream mode", http.StatusInternalServerError) + return + } + defer down.Close() + defer up.Close() + + if _, err := down.Write([]byte("HTTP/1.1 200 OK\n")); err != nil { + Warning.Print(err) + return + } + + if err := resp.Header.Write(down); err != nil { + Warning.Print(err) + return + } + + if _, err := down.Write([]byte("\n")); err != nil { + Warning.Print(err) + return + } + + upDone := make(chan struct{}) + downDone := make(chan struct{}) + go copyStream(down, io.MultiReader(rem, up), upDone) + go copyStream(up, downBuf, downDone) + <-upDone + <-downDone +} + +func copyStream(dst io.Writer, src io.Reader, done chan struct{}) { + defer close(done) + if _, err := io.Copy(dst, src); err != nil { + Warning.Print(err) + } + if c, ok := dst.(interface { + CloseWrite() error + }); ok { + if err := c.CloseWrite(); err != nil { + Warning.Printf("Error closing connection: %s", err) + } + } +} + +func doChunkedResponse(w http.ResponseWriter, resp *http.Response, client *httputil.ClientConn) { + // Because we can't go back to request/response after we + // hijack the connection, we need to close it and make the + // client open another. + w.Header().Add("Connection", "close") + w.WriteHeader(resp.StatusCode) + + down, _, up, rem, err := hijack(w, client) + if err != nil { + http.Error(w, "Unable to hijack response stream for chunked response", http.StatusInternalServerError) + return + } + defer up.Close() + defer down.Close() + // Copy the chunked response body to downstream, + // stopping at the end of the chunked section. + rawResponseBody := io.MultiReader(rem, up) + if _, err := io.Copy(ioutil.Discard, httputil.NewChunkedReader(io.TeeReader(rawResponseBody, down))); err != nil { + http.Error(w, "Error copying chunked response body", http.StatusInternalServerError) + return + } + resp.Trailer.Write(down) + // a chunked response ends with a CRLF + down.Write([]byte("\r\n")) +} + +func hijack(w http.ResponseWriter, client *httputil.ClientConn) (down net.Conn, downBuf *bufio.ReadWriter, up net.Conn, rem io.Reader, err error) { + hj, ok := w.(http.Hijacker) + if !ok { + err = errors.New("Unable to cast to Hijack") + return + } + down, downBuf, err = hj.Hijack() + if err != nil { + return + } + up, rem = client.Hijack() + return +} diff --git a/proxy/common.go b/proxy/common.go new file mode 100644 index 00000000..4d29d95e --- /dev/null +++ b/proxy/common.go @@ -0,0 +1,28 @@ +package proxy + +import ( + "os/exec" + "strings" + + "github.com/fsouza/go-dockerclient" + . "github.com/weaveworks/weave/common" +) + +func callWeave(args ...string) ([]byte, error) { + args = append([]string{"--local"}, args...) + Debug.Print("Calling weave", args) + cmd := exec.Command("./weave", args...) + cmd.Env = []string{"PROCFS=/hostproc", "PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"} + out, err := cmd.CombinedOutput() + return out, err +} + +func weaveCIDRsFromConfig(config *docker.Config) ([]string, bool) { + for _, e := range config.Env { + if strings.HasPrefix(e, "WEAVE_CIDR=") { + result := strings.Trim(e[11:], " ") + return strings.Split(strings.TrimSpace(result), " "), result != "" + } + } + return nil, false +} diff --git a/proxy/create_container_interceptor.go b/proxy/create_container_interceptor.go new file mode 100644 index 00000000..35841a9b --- /dev/null +++ b/proxy/create_container_interceptor.go @@ -0,0 +1,105 @@ +package proxy + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/fsouza/go-dockerclient" +) + +type createContainerInterceptor struct { + client *docker.Client + withDNS bool + dockerBridgeIP string +} + +type createContainerRequestBody struct { + *docker.Config + HostConfig *docker.HostConfig `json:"HostConfig,omitempty" yaml:"HostConfig,omitempty"` + MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty"` +} + +func (i *createContainerInterceptor) InterceptRequest(r *http.Request) error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + r.Body.Close() + + container := createContainerRequestBody{} + if err := json.Unmarshal(body, &container); err != nil { + return err + } + + if _, ok := weaveCIDRsFromConfig(container.Config); ok { + container.HostConfig.VolumesFrom = append(container.HostConfig.VolumesFrom, "weaveproxy") + if err := i.setWeaveWaitEntrypoint(container.Config); err != nil { + return err + } + if err := i.setWeaveDNS(&container); err != nil { + return err + } + } + + newBody, err := json.Marshal(container) + if err != nil { + return err + } + r.Body = ioutil.NopCloser(bytes.NewReader(newBody)) + r.ContentLength = int64(len(newBody)) + + return nil +} + +func (i *createContainerInterceptor) setWeaveWaitEntrypoint(container *docker.Config) error { + var image *docker.Config + if !configHasEntrypoint(container) || !configHasCmd(container) { + imageInfo, err := i.client.InspectImage(container.Image) + if err != nil { + return err + } + image = imageInfo.Config + } + + var entry, command []string + if configHasEntrypoint(container) { + entry = container.Entrypoint + } else if configHasEntrypoint(image) { + entry = image.Entrypoint + } + if configHasCmd(container) { + command = container.Cmd + } else if configHasCmd(image) { + command = image.Cmd + } + + container.Entrypoint = []string{"/home/weavewait/weavewait"} + container.Cmd = append(entry, command...) + return nil +} + +func configHasEntrypoint(c *docker.Config) bool { + return c != nil && len(c.Entrypoint) > 0 +} +func configHasCmd(c *docker.Config) bool { + return c != nil && len(c.Cmd) > 0 +} + +func (i *createContainerInterceptor) setWeaveDNS(container *createContainerRequestBody) error { + if !i.withDNS { + return nil + } + + container.HostConfig.DNS = append(container.HostConfig.DNS, i.dockerBridgeIP) + + if len(container.HostConfig.DNSSearch) == 0 { + container.HostConfig.DNSSearch = []string{"."} + } + return nil +} + +func (i *createContainerInterceptor) InterceptResponse(r *http.Response) error { + return nil +} diff --git a/proxy/interceptor.go b/proxy/interceptor.go new file mode 100644 index 00000000..69d903e2 --- /dev/null +++ b/proxy/interceptor.go @@ -0,0 +1,19 @@ +package proxy + +import "net/http" + +type interceptor interface { + InterceptRequest(*http.Request) error + InterceptResponse(*http.Response) error +} + +type nullInterceptor struct { +} + +func (i nullInterceptor) InterceptRequest(r *http.Request) error { + return nil +} + +func (i nullInterceptor) InterceptResponse(r *http.Response) error { + return nil +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 00000000..7681230e --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,80 @@ +package proxy + +import ( + "net" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/fsouza/go-dockerclient" + . "github.com/weaveworks/weave/common" +) + +var ( + containerCreateRegexp = regexp.MustCompile("/v[0-9\\.]*/containers/create") + containerStartRegexp = regexp.MustCompile("^/v[0-9\\.]*/containers/[^/]*/(re)?start$") +) + +type Proxy struct { + Dial func() (net.Conn, error) + client *docker.Client + withDNS bool + dockerBridgeIP string +} + +func NewProxy(targetURL string, withDNS bool) (*Proxy, error) { + u, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + var dockerBridgeIP []byte + if withDNS { + dockerBridgeIP, err = callWeave("docker-bridge-ip") + if err != nil { + return nil, err + } + } + + client, err := docker.NewClient(targetURL) + if err != nil { + return nil, err + } + + targetAddr := "" + switch u.Scheme { + case "tcp": + targetAddr = u.Host + case "unix": + targetAddr = u.Path + } + + return &Proxy{ + Dial: func() (net.Conn, error) { + return net.Dial(u.Scheme, targetAddr) + }, + client: client, + withDNS: withDNS, + dockerBridgeIP: string(dockerBridgeIP), + }, nil +} + +func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + Info.Printf("%s %s", r.Method, r.URL) + path := r.URL.Path + switch { + case containerCreateRegexp.MatchString(path): + proxy.serveWithInterceptor(&createContainerInterceptor{proxy.client, proxy.withDNS, proxy.dockerBridgeIP}, w, r) + case containerStartRegexp.MatchString(path): + proxy.serveWithInterceptor(&startContainerInterceptor{proxy.client, proxy.withDNS}, w, r) + case strings.HasPrefix(path, "/weave"): + w.WriteHeader(http.StatusOK) + default: + proxy.serveWithInterceptor(&nullInterceptor{}, w, r) + } +} + +func (proxy *Proxy) serveWithInterceptor(i interceptor, w http.ResponseWriter, r *http.Request) { + newClient(proxy.Dial, i).ServeHTTP(w, r) +} diff --git a/proxy/start_container_interceptor.go b/proxy/start_container_interceptor.go new file mode 100644 index 00000000..f2e361e4 --- /dev/null +++ b/proxy/start_container_interceptor.go @@ -0,0 +1,50 @@ +package proxy + +import ( + "net/http" + "regexp" + "strings" + + "github.com/fsouza/go-dockerclient" + . "github.com/weaveworks/weave/common" +) + +var containerIDRegexp = regexp.MustCompile("^/v[0-9\\.]*/containers/([^/]*)/.*") + +type startContainerInterceptor struct { + client *docker.Client + withDNS bool +} + +func (i *startContainerInterceptor) InterceptRequest(r *http.Request) error { + return nil +} + +func (i *startContainerInterceptor) InterceptResponse(r *http.Response) error { + if subs := containerIDRegexp.FindStringSubmatch(r.Request.URL.Path); subs == nil { + Warning.Printf("No container id found in request with path %s", r.Request.URL.Path) + return nil + } + containerID := subs[1] + + container, err := i.client.InspectContainer(containerID) + if err != nil { + Warning.Printf("Error inspecting container %s: %v", containerID, err) + return nil + } + + cidrs, ok := weaveCIDRsFromConfig(container.Config) + if !ok { + Debug.Print("No Weave CIDR, ignoring") + return nil + } + Info.Printf("Container %s was started with CIDR \"%s\"", containerID, strings.Join(cidrs, " ")) + args := []string{"attach"} + args = append(args, cidrs...) + args = append(args, containerID) + if _, err := callWeave(args...); err != nil { + Warning.Printf("Attaching container %s to weave failed: %v", containerID, err) + return nil + } + return nil +} diff --git a/site/features.md b/site/features.md index 1e0ecab5..dfee4b13 100644 --- a/site/features.md +++ b/site/features.md @@ -24,6 +24,7 @@ example](https://github.com/weaveworks/weave#example): * [Fault tolerance](#fault-tolerance) * [Automatic IP address management](#ipam) * [Automatic discovery with WeaveDNS](#dns) + * [Proxy](#proxy) ### Virtual Ethernet Switch @@ -403,4 +404,11 @@ to enable and use this feature. WeaveDNS is a distributed DNS service for weave networks, enabling containers to address each other by name rather than IP address. Find -out more about weaveDNS from its [documentation](weavedns.html). +out more about WeaveDNS from its [documentation](weavedns.html). + +### Proxy + +The weave proxy enables you to use the docker command-line utility, allowing +weave-enabled containers to be started in the foreground, and to wait for the +weave network interface before running. Find out more about the weave proxy +from its [documentation](proxy.html). diff --git a/site/proxy.md b/site/proxy.md new file mode 100644 index 00000000..c1d0daa0 --- /dev/null +++ b/site/proxy.md @@ -0,0 +1,59 @@ +--- +title: Weave Proxy +layout: default +--- + +# Weave Proxy + +Instead of the weave command-line utility, you may prefer to use the standard +docker command-line utility to manage containers. This will allow containers to +be started in foreground mode. It will also ensure that the weave network +interface is available before running your container. + +## Setup + +To do this, while still connecting containers to the weave network, you can use +the weave proxy. We can start the proxy with + + host1$ weave launch-proxy + +By default, the proxy will connect to docker at `unix:///var/run/docker.sock`, +and listen on port 12375. However, we can adjust the connection to docker via +the `-H` argument. All docker commands can be run via the proxy, so it is safe +to globally adjust your `DOCKER_HOST`. + + host1$ export DOCKER_HOST="tcp://host1:12375" + host1$ docker ps + +## Usage + +When containers are created via the weave proxy, their entrypoint will be +modified to wait for the weave network interface to become available. When they are +started via the weave proxy, any IP addresses and networks specified in the +`WEAVE_CIDR` environment variable will be attached. We can create and start a +container via the weave proxy with + + host1$ docker run -e WEAVE_CIDR=10.2.1.1/24 -ti ubuntu /bin/sh + +or, equivalently with + + host1$ docker create -e WEAVE_CIDR=10.2.1.1/24 -ti ubuntu /bin/sh + 5ef831df61d50a1a49272357155a976595e7268e590f0a2c75693337b14e1382 + host1$ docker start 5ef831df61d50a1a49272357155a976595e7268e590f0a2c75693337b14e1382 + +Multiple IP addresses and networks can be supplied in the WEAVE_CIDR variable by space-separating them, as in `WEAVE_CIDR=10.2.1.1/24 10.2.2.1/24`. + +## Usage with WeaveDNS + +Containers started via the proxy can be automatically configured to use WeaveDNS for name resolution. To accomplish this we need to launch the proxy with the `--with-dns` option + + host1$ weave launch + host1$ weave launch-dns 10.2.254.1/24 + host1$ weave launch-proxy --with-dns + +With this done, any containers launched through the proxy will use weaveDNS for name resolution. WeaveDNS is used in addition to any dns servers specified via the `--dns` option. More details on weaveDNS can be found in the [weaveDNS documentation](weavedns.html). + +## Limitations + +* The proxy does not currently support TLS. +* If you have a firewall, you will need to make sure port 12375 is open. diff --git a/test/400_weave_local_test.sh b/test/400_weave_local_test.sh index c4f9eab4..d86e2ece 100755 --- a/test/400_weave_local_test.sh +++ b/test/400_weave_local_test.sh @@ -10,7 +10,7 @@ run_on $HOST1 sudo ./weave --local launch -iprange 10.2.5.0/24 assert_raises "docker_on $HOST1 ps | grep weave" run_on $HOST1 sudo ./weave --local run 10.2.6.5/24 -ti --name=c1 gliderlabs/alpine /bin/sh -assert_raises "exec_on $HOST1 c1 ifconfig | grep ethwe" +assert_raises "exec_on $HOST1 c1 ip link show ethwe" run_on $HOST1 sudo ./weave --local run -ti --name=c2 gliderlabs/alpine /bin/sh assert_raises "exec_on $HOST1 c2 ifconfig | grep ethwe" diff --git a/test/600_proxy_docker_py_test.sh b/test/600_proxy_docker_py_test.sh new file mode 100755 index 00000000..dc5e3282 --- /dev/null +++ b/test/600_proxy_docker_py_test.sh @@ -0,0 +1,16 @@ +#! /bin/bash + +. ./config.sh + +start_suite "Run docker-py test suite against the proxy" + +weave_on $HOST1 launch-proxy +assert_raises "docker_on $HOST1 ps | grep weaveproxy" +assert_raises "docker_on $HOST1 run \ + -e NOT_ON_HOST=true \ + -e DOCKER_HOST=tcp://172.17.42.1:12375 \ + -v /tmp:/tmp \ + -v /var/run/docker.sock:/var/run/docker.sock \ + joffrey/docker-py python tests/integration_test.py" + +end_suite diff --git a/test/610_proxy_wait_for_weave_test.sh b/test/610_proxy_wait_for_weave_test.sh new file mode 100755 index 00000000..21504432 --- /dev/null +++ b/test/610_proxy_wait_for_weave_test.sh @@ -0,0 +1,15 @@ +#! /bin/bash + +. ./config.sh + +start_suite "Proxy waits for weave to be ready before running container commands" +weave_on $HOST1 launch-proxy +BASE_IMAGE=busybox +# Ensure the base image does not exist, so that it will be pulled +if (docker_on $HOST1 images $BASE_IMAGE | grep $BASE_IMAGE); then + docker_on $HOST1 rmi $BASE_IMAGE +fi + +assert_raises "docker_proxy_on $HOST1 run -e 'WEAVE_CIDR=10.2.1.1/24' $BASE_IMAGE ip link show ethwe | grep 'state UP'" + +end_suite diff --git a/test/620_proxy_entrypoint_inheritance_test.sh b/test/620_proxy_entrypoint_inheritance_test.sh new file mode 100755 index 00000000..2bb2fce6 --- /dev/null +++ b/test/620_proxy_entrypoint_inheritance_test.sh @@ -0,0 +1,14 @@ +#! /bin/bash + +. ./config.sh + +start_suite "Proxy uses entrypoint from the image with weavewait" +weave_on $HOST1 launch-proxy +docker_on $HOST1 build -t inspect-ethwe - <<- EOF + FROM gliderlabs/alpine + ENTRYPOINT ["ip", "link", "show", "ethwe"] +EOF + +assert_raises "docker_proxy_on $HOST1 run -e 'WEAVE_CIDR=10.2.1.1/24' inspect-ethwe | grep 'state UP'" + +end_suite diff --git a/test/630_proxy_dns_test.sh b/test/630_proxy_dns_test.sh new file mode 100755 index 00000000..e4b94fee --- /dev/null +++ b/test/630_proxy_dns_test.sh @@ -0,0 +1,18 @@ +#! /bin/bash + +. ./config.sh + +C1=10.2.0.78 +C2=10.2.0.34 +NAME=seetwo.weave.local + +start_suite "Proxy registers containers with dns" + +weave_on $HOST1 launch-dns 10.2.254.1/24 +weave_on $HOST1 launch-proxy --with-dns +docker_proxy_on $HOST1 run -e WEAVE_CIDR=$C2/24 -dt --name=c2 -h $NAME gliderlabs/alpine /bin/sh +docker_proxy_on $HOST1 run -e WEAVE_CIDR=$C1/24 -dt --name=c1 aanand/docker-dnsutils /bin/sh + +assert_dns_record $HOST1 c1 $NAME $C2 + +end_suite diff --git a/test/640_proxy_restart_reattaches_test.sh b/test/640_proxy_restart_reattaches_test.sh new file mode 100755 index 00000000..01017764 --- /dev/null +++ b/test/640_proxy_restart_reattaches_test.sh @@ -0,0 +1,20 @@ +#! /bin/bash + +. ./config.sh + +C1=10.2.0.78 +C2=10.2.0.34 +NAME=seetwo.weave.local + +start_suite "Proxy restart reattaches networking to containers" + +weave_on $HOST1 launch-dns 10.2.254.1/24 +weave_on $HOST1 launch-proxy --with-dns +docker_proxy_on $HOST1 run -e WEAVE_CIDR=$C2/24 -dt --name=c2 -h $NAME gliderlabs/alpine /bin/sh +docker_proxy_on $HOST1 run -e WEAVE_CIDR=$C1/24 -dt --name=c1 aanand/docker-dnsutils /bin/sh + +docker_proxy_on $HOST1 restart c2 +assert_raises "exec_on $HOST1 c2 ip link show ethwe | grep 'state UP'" +assert_dns_record $HOST1 c1 $NAME $C2 + +end_suite diff --git a/test/650_proxy_entrypoint_override_test.sh b/test/650_proxy_entrypoint_override_test.sh new file mode 100755 index 00000000..4f826408 --- /dev/null +++ b/test/650_proxy_entrypoint_override_test.sh @@ -0,0 +1,14 @@ +#! /bin/bash + +. ./config.sh + +start_suite "Proxy allows overridden entrypoint from the container with weavewait" +weave_on $HOST1 launch-proxy +docker_on $HOST1 build -t false - <<- EOF + FROM gliderlabs/alpine + ENTRYPOINT ["/bin/false"] +EOF + +assert_raises "docker_proxy_on $HOST1 run -e 'WEAVE_CIDR=10.2.1.1/24' --entrypoint '/sbin/ip' false link show ethwe | grep 'state UP'" + +end_suite diff --git a/test/config.sh b/test/config.sh index 3983c826..48de3afb 100644 --- a/test/config.sh +++ b/test/config.sh @@ -76,6 +76,13 @@ docker_on() { docker -H tcp://$host:2375 "$@" } +docker_proxy_on() { + host=$1 + shift 1 + greyly echo "Docker (with proxy) on $host: $@" >&2 + docker -H tcp://$host:12375 "$@" +} + weave_on() { host=$1 shift 1 diff --git a/weave b/weave index 5cf1da6e..744df836 100755 --- a/weave +++ b/weave @@ -27,6 +27,7 @@ usage() { echo "weave setup" echo "weave launch [-password ] [-nickname ] [-iprange ] ..." echo "weave launch-dns " + echo "weave launch-proxy [-H ] [--with-dns]" echo "weave connect " echo "weave forget " echo "weave run [--with-dns] [ ...] ..." @@ -679,6 +680,63 @@ get_ipam_cidr() { fi } +###################################################################### +# weave proxy helpers +###################################################################### + +PROXY_PORT=12375 +PROXY_CONTAINER_NAME=weaveproxy + +proxy_host() { + case $DOCKER_HOST in + tcp://*) + PROXY_HOST=$(echo $DOCKER_HOST | sed -e "s|tcp://\([^:]*\):[0-9]*|\1|") + ;; + *) + PROXY_HOST="localhost" + ;; + esac +} + +wait_for_proxy_to_boot() { + proxy_host + for n in `seq 1 10`; do + if curl --max-time 1 --connect-timeout 1 -s -X GET http://$PROXY_HOST:$PROXY_PORT/weave ; then + return 0 + fi + sleep 1 + done + echo "Proxy did not start within 10 seconds" >&2 + return 1 +} + +ensure_proxy_running() { + case $(docker inspect --format '{{.State.Running}} {{.Config.Image}}' $PROXY_CONTAINER_NAME 2>/dev/null) in + "true $EXEC_IMAGE") + return 0 + ;; + true*) + echo "Proxy already running with unknown image" >&2 + exit 1 + ;; + false*) + docker rm -f $PROXY_CONTAINER_NAME >/dev/null + ;; + esac + # Set WEAVEPROXY_DOCKER_ARGS in the environment in order to supply + # additional parameters, such as resource limits, to docker + # when launching the weave proxy container. + PROXY_CONTAINER=$(docker run --privileged --net=host -d \ + --name=$PROXY_CONTAINER_NAME \ + -p $PROXY_PORT:$PROXY_PORT/tcp -v /var/run/docker.sock:/var/run/docker.sock \ + -v /proc:/hostproc -e PROCFS=/hostproc \ + --entrypoint=/home/weave/weaveproxy \ + $WEAVE_DOCKER_ARGS $WEAVEPROXY_DOCKER_ARGS \ + $EXEC_IMAGE "$@") + + wait_for_proxy_to_boot +} + ########################################################################################## [ $(id -u) = 0 ] || { @@ -807,6 +865,10 @@ case "$COMMAND" in populate_dns echo $DNS_CONTAINER ;; + launch-proxy) + ensure_proxy_running "$@" + echo $PROXY_CONTAINER + ;; connect) [ $# -eq 1 ] || usage http_call $CONTAINER_NAME $HTTP_PORT POST /connect -d "peer=$1" @@ -859,6 +921,10 @@ case "$COMMAND" in dns_args "$@" echo -n $DNS_ARGS ;; + docker-bridge-ip) + docker_bridge_ip + echo -n $DOCKER_BRIDGE_IP + ;; start) collect_cidr_args "$@" shift $CIDR_COUNT diff --git a/weaveexec/Dockerfile b/weaveexec/Dockerfile index e7134926..65b0f47a 100644 --- a/weaveexec/Dockerfile +++ b/weaveexec/Dockerfile @@ -15,6 +15,10 @@ WORKDIR /home/weave ADD ./weave /home/weave/ ADD ./sigproxy /home/weave/ +ADD ./weaveproxy /home/weave/ +ADD ./weavewait /home/weavewait/weavewait ADD ./docker.tgz / +VOLUME /home/weavewait + ENTRYPOINT ["/home/weave/sigproxy", "/home/weave/weave"] diff --git a/weaveproxy/main.go b/weaveproxy/main.go new file mode 100644 index 00000000..2a395ae1 --- /dev/null +++ b/weaveproxy/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "net/http" + + "code.google.com/p/getopt" + . "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/proxy" +) + +var ( + defaultTarget = "unix:///var/run/docker.sock" + defaultListen = ":12375" +) + +func main() { + var target, listen string + var withDNS, debug bool + + getopt.BoolVarLong(&debug, "debug", 'd', "log debugging information") + getopt.StringVar(&target, 'H', fmt.Sprintf("docker daemon URL to proxy (default %s)", defaultTarget)) + getopt.StringVar(&listen, 'L', fmt.Sprintf("address on which to listen (default %s)", defaultListen)) + getopt.BoolVarLong(&withDNS, "with-dns", 'w', "instruct created containers to use weaveDNS as their nameserver") + getopt.Parse() + + if target == "" { + target = defaultTarget + } + + if listen == "" { + listen = defaultListen + } + + if debug { + InitDefaultLogging(true) + } + + p, err := proxy.NewProxy(target, withDNS) + if err != nil { + Error.Fatalf("Could not start proxy: %s", err) + } + s := &http.Server{ + Addr: listen, + Handler: p, + } + + Info.Printf("Listening on %s", listen) + Info.Printf("Proxying %s", target) + + if err := s.ListenAndServe(); err != nil { + Error.Fatalf("Could not listen on %s: %s", listen, err) + } +} diff --git a/weavewait/main.go b/weavewait/main.go new file mode 100644 index 00000000..c4f52549 --- /dev/null +++ b/weavewait/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + + "github.com/weaveworks/weave/net" +) + +func main() { + if _, err := net.EnsureInterface("ethwe", 20); err != nil { + fmt.Fprintln(os.Stderr, err) + } + + if len(os.Args) <= 1 { + os.Exit(0) + } + cmd := exec.Command(os.Args[1], os.Args[2:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + os.Exit(1) + } +}