Skip to content

Commit

Permalink
[proxy] More flexible container name -> hostname derivation
Browse files Browse the repository at this point in the history
Provides a new "--hostname=substitution" flag to WeaveProxy, where
'substitution' is a sed-style substitution command. 'substitution' will be
applied to container names at launch-time, as a means to control what hostnames will
be registered in WeaveDNS.

The substitution is implemented with
https://golang.org/pkg/regexp/#Regexp.ReplaceAllString . Thus, references to
regexp substitution groups should be prepended with a dollar sign.

For instance, if we provide "--hostname='/aws-[0-9]+-(.*)/my-app-$1/'", running
a container named 'aws-12798186823-foo' through WeaveProxy will lead to WeaveDNS
registering 'my-app-foo'.

Closes #1018
  • Loading branch information
Alfonso Acosta committed Jul 14, 2015
1 parent bbacaa4 commit d91beb9
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 12 deletions.
1 change: 1 addition & 0 deletions prog/weaveproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func main() {
mflag.BoolVar(&justVersion, []string{"#version", "-version"}, false, "print version and exit")
mflag.StringVar(&logLevel, []string{"-log-level"}, "info", "logging level (debug, info, warning, error)")
ListVar(&c.ListenAddrs, []string{"H"}, defaultListenAddrs, "addresses on which to listen")
mflag.StringVar(&c.Hostname, []string{"-hostname"}, "/(.*)/$1/", "Regexp substitution to apply on container names (e.g. '/aws-[0-9]+-(.*)/my-app-$1/')")
mflag.BoolVar(&c.NoDefaultIPAM, []string{"#-no-default-ipam", "-no-default-ipalloc"}, false, "do not automatically allocate addresses for containers without a WEAVE_CIDR")
mflag.BoolVar(&c.NoRewriteHosts, []string{"no-rewrite-hosts"}, false, "do not automatically rewrite /etc/hosts. Use if you need the docker IP to remain in /etc/hosts")
mflag.StringVar(&c.TLSConfig.CACert, []string{"#tlscacert", "-tlscacert"}, "", "Trust certs signed only by this CA")
Expand Down
18 changes: 18 additions & 0 deletions proxy/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,21 @@ func inspectContainerInPath(client *docker.Client, path string) (*docker.Contain
}
return container, err
}

// Call FindSubmatch and return a map from named groups to submatches
// (Golang's regexp package doesn't provide a way to do it directly)
func findStringNamedSubmatch(re *regexp.Regexp, s string) map[string]string {
namedSubmatches := make(map[string]string)

submatches := re.FindStringSubmatch(s)
groupNames := re.SubexpNames()

for i, submatch := range submatches {
name := groupNames[i]
if len(name) > 0 {
namedSubmatches[name] = submatch
}
}

return namedSubmatches
}
19 changes: 19 additions & 0 deletions proxy/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package proxy

import (
"reflect"
"regexp"
"testing"
)

func TestFindStringNamedSubmatch(t *testing.T) {
re := regexp.MustCompile(`^(prefix) (?P<fooGroup>foo)(?P<barGroup>bar) (suffix)$`)
expectedResult := map[string]string{
"fooGroup": "foo",
"barGroup": "bar",
}
namedSubmatches := findStringNamedSubmatch(re, "prefix foobar suffix")
if !reflect.DeepEqual(namedSubmatches, expectedResult) {
t.Error("Unexpected result", namedSubmatches)
}
}
4 changes: 3 additions & 1 deletion proxy/create_container_interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ func (i *createContainerInterceptor) InterceptRequest(r *http.Request) error {
if err := i.setWeaveWaitEntrypoint(container.Config); err != nil {
return err
}
if err := i.setWeaveDNS(&container, r.URL.Query().Get("name")); err != nil {
containerName := r.URL.Query().Get("name")
hostname := i.proxy.hostnamePattern.ReplaceAllString(containerName, i.proxy.hostnameReplacement)
if err := i.setWeaveDNS(&container, hostname); err != nil {
return err
}
}
Expand Down
58 changes: 48 additions & 10 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package proxy

import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
Expand All @@ -15,20 +16,25 @@ import (
)

const (
defaultCaFile = "ca.pem"
defaultKeyFile = "key.pem"
defaultCertFile = "cert.pem"
dockerSock = "/var/run/docker.sock"
dockerSockUnix = "unix://" + dockerSock
defaultCaFile = "ca.pem"
defaultKeyFile = "key.pem"
defaultCertFile = "cert.pem"
dockerSock = "/var/run/docker.sock"
dockerSockUnix = "unix://" + dockerSock
substitutionPatternName = "pattern"
substitutionReplacementName = "replacement"
)

var (
containerCreateRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/create$")
containerStartRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/[^/]*/(re)?start$")
execCreateRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/[^/]*/exec$")
containerCreateRegexp = regexp.MustCompile(`^(/v[0-9\.]*)?/containers/create$`)
containerStartRegexp = regexp.MustCompile(`^(/v[0-9\.]*)?/containers/[^/]*/(re)?start$`)
execCreateRegexp = regexp.MustCompile(`^(/v[0-9\.]*)?/containers/[^/]*/exec$`)
substitutionRegexp = regexp.MustCompile(`^/(?P<` + substitutionPatternName + `>(\\/|[^/])*)/(?P<` + substitutionReplacementName + `>(\\/|[^/])*)/$`)
escapedSlashRegexp = regexp.MustCompile(`\\/`)
)

type Config struct {
Hostname string
ListenAddrs []string
NoDefaultIPAM bool
NoRewriteHosts bool
Expand All @@ -40,8 +46,10 @@ type Config struct {

type Proxy struct {
Config
client *docker.Client
dockerBridgeIP string
client *docker.Client
dockerBridgeIP string
hostnamePattern *regexp.Regexp
hostnameReplacement string
}

func NewProxy(c Config) (*Proxy, error) {
Expand All @@ -65,6 +73,11 @@ func NewProxy(c Config) (*Proxy, error) {
p.dockerBridgeIP = string(dockerBridgeIP)
}

p.hostnamePattern, p.hostnameReplacement, err = parseHostname(c.Hostname)
if err != nil {
return nil, err
}

return p, nil
}

Expand Down Expand Up @@ -119,6 +132,31 @@ func (proxy *Proxy) ListenAndServe() {
}
}

func unescapeBackSlashes(src string) string {
return escapedSlashRegexp.ReplaceAllString(src, "/")
}

func parseHostname(hostname string) (*regexp.Regexp, string, error) {
namedSubmatches := findStringNamedSubmatch(substitutionRegexp, hostname)
if len(namedSubmatches) < 1 {
errStr := fmt.Sprintf("Incorrect hostname expression: %s", hostname)
return nil, "", errors.New(errStr)
}

escapedPattern := namedSubmatches[substitutionPatternName]
escapedReplacement := namedSubmatches[substitutionReplacementName]
unescapedPattern := unescapeBackSlashes(escapedPattern)
unescapedReplacement := unescapeBackSlashes(escapedReplacement)

patternRegexp, err := regexp.Compile(unescapedPattern)
if err != nil {
errStr := fmt.Sprintf("Incorrect hostname pattern '%s': %s", escapedPattern, err.Error())
return nil, "", errors.New(errStr)
}

return patternRegexp, unescapedReplacement, nil
}

func copyOwnerAndPermissions(from, to string) error {
stat, err := os.Stat(from)
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions proxy/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package proxy

import (
"testing"
)

func TestParseHostname(t *testing.T) {
hostnameExpr := `/aws-[0-9]+-(.*)/my-app-$1/`

patternRegexp, replacementStr, err := parseHostname(hostnameExpr)
if err != nil {
t.Error("Unexpected error", err)
}
if replacementStr != "my-app-$1" {
t.Error("Unexpected replacement string", replacementStr)
}

hostname := patternRegexp.ReplaceAllString("aws-17651276812-name", replacementStr)
if hostname != "my-app-name" {
t.Error("Unexpected replacement hostname", hostname)
}
}

func TestParseWrongHostname(t *testing.T) {
wrongHostnameExprs := []string{
`/`,
`//`,
`/ /`,
`/ / / /`,
`/aws-[0-9]+-(.*)/my-app-$1/ /`,
`/ /aws-[0-9]+-(.*)/my-app-$1/`,
`/ /aws-[0-9]+-(.*)/my-app-$1/ /`,
}

for _, hostnameExpr := range wrongHostnameExprs {
_, _, err := parseHostname(hostnameExpr)
if err == nil {
t.Error("Should not succesully parse wrong hostname expression", hostnameExpr)
}
}
}

func TestEscapedHostname(t *testing.T) {
hostnameExpr := `/aws\/[0-9]+\/(.*)/my\/app\/$1/`

patternRegexp, replacementStr, err := parseHostname(hostnameExpr)
if err != nil {
t.Error("Unexpected error", err)
}
if replacementStr != "my/app/$1" {
t.Error("Unexpected replacement string", replacementStr)
}

hostname := patternRegexp.ReplaceAllString("aws/17651276812/name", replacementStr)
if hostname != "my/app/name" {
t.Error("Unexpected replacement hostname", hostname)
}

}
27 changes: 27 additions & 0 deletions site/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,33 @@ automatically if it is running at the point when they are started -
see the [weaveDNS usage](weavedns.html#usage) section for an in-depth
explanation of the behaviour and how to control it.

Typicaly, the proxy will pass on container names as-is to [weaveDNS](weavedns.html)
for registration. However, there are situations in which the final container
name is out of the user's control (e.g. when using Docker orchestrators which
append control/namespacing identifiers to the original container names).

For those situations, the proxy provides a
`--hostname=/containerNamePattern/hostnameReplacement/` flag, where
`/containerNamePattern/hostnameReplacement/` is a regular expression
substitution command.

For instance, if we launch the proxy using `--hostname='/aws-[0-9]+-(.*)/my-app-$1/'`

host1$ weave launch-router && weave launch-dns && weave launch-proxy --hostname='/aws-[0-9]+-(.*)/my-app-$1/'
host1$ eval "$(weave env)"

then, running a container named `aws-12798186823-foo` will lead to weaveDNS registering
hostname `my-app-foo` and not `aws-12798186823-foo`.

host1$ docker run -ti --name=aws-12798186823-foo ubuntu ping my-app-foo
PING my-app-foo.weave.local (10.32.0.2) 56(84) bytes of data.
64 bytes from my-app-foo.weave.local (10.32.0.2): icmp_seq=1 ttl=64 time=0.027 ms
64 bytes from my-app-foo.weave.local (10.32.0.2): icmp_seq=2 ttl=64 time=0.067 ms

Note how regexp substitution groups should be prepended with a dollar sign
(e.g. `$1`). For further details on the regular expression syntax please see
[Google's re2 documentation](https://github.com/google/re2/wiki/Syntax).

## <a name="tls"></a>Securing the docker communication with TLS

If you are
Expand Down
36 changes: 36 additions & 0 deletions test/680_proxy_hostname_derivation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#! /bin/bash

. ./config.sh

C1=10.2.0.78
C2=10.2.0.34
C3=10.2.0.57
CNAME1=qiuds71y827hdi-seeone-1io9qd9i0wd
NAME1=seeone.weave.local
CNAME2=124DJKSNK812-seetwo-128hbaJ881
NAME2=seetwo.weave.local
CNAME3=doesnotmatchpattern
NAME3=doesnotmatchpattern.weave.local

start_container() {
proxy docker_on $HOST1 run "$@" -dt $DNS_IMAGE /bin/sh
}

start_suite "Hostname derivation through container name substitutions"

weave_on $HOST1 launch-dns 10.2.254.1/24
weave_on $HOST1 launch-proxy --hostname '/[^-]+-(?P<appname>[^-]*)-[^-]+/$appname/'

start_container -e WEAVE_CIDR=$C1/24 --name=$CNAME1
start_container -e WEAVE_CIDR=$C2/24 --name=$CNAME2
start_container -e WEAVE_CIDR=$C3/24 --name=$CNAME3

check() {
assert "proxy exec_on $HOST1 $1 getent hosts $2 | tr -s ' '" "$3 $2"
}

check $CNAME1 $NAME2 $C2
check $CNAME2 $NAME3 $C3
check $CNAME3 $NAME1 $C1

end_suite
2 changes: 1 addition & 1 deletion weave
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ weave launch-router [--password <password>] [--nickname <nickname>]
[--no-discovery] [--init-peer-count <count>] <peer> ...
weave launch-dns [<addr>]
weave launch-proxy [-H <endpoint>] [--with-dns | --without-dns]
[--no-default-ipalloc] [--no-rewrite-hosts]
[--no-default-ipalloc] [--no-rewrite-hosts] [--hostname]
weave connect [--replace] [<peer> ...]
weave forget <peer> ...
weave run [--with-dns | --without-dns] [<addr> ...]
Expand Down

0 comments on commit d91beb9

Please sign in to comment.