[
  {
    "path": ".dockerignore",
    "content": ".git\n"
  },
  {
    "path": ".gitignore",
    "content": "release\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:3.2\nENTRYPOINT [\"/bin/connectable\"]\nCOPY . /go/src/github.com/gliderlabs/connectable\nRUN apk add --update go git mercurial iptables \\\n  && cd /go/src/github.com/gliderlabs/connectable \\\n  && export GOPATH=/go \\\n  && go get \\\n  && go build -ldflags \"-X main.Version $(cat VERSION)\" -o /bin/connectable \\\n  && apk del go git mercurial \\\n  && rm -rf /go /var/cache/apk/*\n"
  },
  {
    "path": "Dockerfile.dev",
    "content": "FROM gliderlabs/alpine:3.2\nRUN apk-install go git mercurial iptables\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Glider Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "NAME=connectable\nREPO=gliderlabs\nVERSION=$(shell cat VERSION)\n\ndev:\n\t@docker history $(NAME):dev &> /dev/null \\\n\t\t|| docker build -f Dockerfile.dev -t $(NAME):dev .\n\t@docker run --rm --name $(NAME)-dev \\\n\t\t-v /var/run/docker.sock:/var/run/docker.sock \\\n\t\t-v $(PWD):/go/src/github.com/$(REPO)/$(NAME) \\\n\t\t$(NAME):dev\n"
  },
  {
    "path": "README.md",
    "content": "# Connectable\n\n[![Docker Hub](https://img.shields.io/badge/docker-ready-blue.svg)](https://registry.hub.docker.com/u/gliderlabs/connectable/)\n[![IRC Channel](https://img.shields.io/badge/irc-%23gliderlabs-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#gliderlabs)\n\nA smart Docker proxy that lets your containers connect to other containers via service\ndiscovery *without being service discovery aware*.\n\n## Getting Connectable\n\nYou can get the Connectable micro container from the Docker Hub.\n\n\t$ docker pull gliderlabs/connectable\n\n## Using Connectable\n\nBasic overview is:\n\n 1. Run a service registry like Consul, perhaps with Registrator\n 1. Start a Connectable container on each host\n 1. Expose Connectable to your containers, using links or Resolvable (experimental)\n 1. Run containers with labels defining what they need to connect to with what ports\n 1. Have software in those containers connect via those ports on localhost\n\n#### Starting Connectable\n\nOnce you have a service registry set up, you point Connectable to it when you launch it.\nYou also need to mount the Docker socket. Here is an example using the local Consul agent, assuming you're running Resolvable:\n\n\t$ docker run -d --name connectable \\\n\t\t\t-v /var/run/docker.sock:/var/run/docker.sock \\\n\t\t\tgliderlabs/connectable:latest\n\nWith Resolvable running, it will have access to Consul DNS. It will be able to resolve any connections using DNS names.\n\n#### Start containers that use Connectable\n\nAll you have to do is specify a port to use and what you'd like to connect to as a label. For example:\n\n\tconnect.6000=redis.service.consul\n\nWith this label set, you can connect to Redis on localhost:6000. You can also specify multiple services:\n\n\t$ docker run -d --name myservice \\\n\t\t\t-l connect.6000=redis.service.consul \\\n\t\t\t-l connect.3306=master.mysql.service.consul \\\n\t\t\texample/myservice\n\n## Load Balancing\n\nConnectable acts as a load balancer across instance of services it finds. It shuffles them randomly on new connections. Although this seems less predictable, it ensures even balancing cluster-wide.\n\nConnectable is a reverse proxy and balancer, but it is not recommended to be used as your public facing balancer. Instead, use a more configurable balancer like haproxy or Nginx. Use Connectable for internal service-to-service connections. For example, you could use Connectable *with* Nginx to simplify your Nginx container setup.\n\n## Health Checking\n\nCurrently Connectable does not have native health checking integration. For now, Connectable defers to the registry to return healthy services. For example, this is how Consul DNS works. Otherwise, when Connectable tries to connect to an endpoint and is unable to connect, it will try the next one transparently until all services have been tried. This covers some but not all \"unhealthy\" service cases.\n\nFuture modules may add support for integration with health checking mechanisms.\n\n## Overhead\n\nLike all proxies, you incur overhead to your connections. Connectable is roughly comparable but slightly slower than Nginx. Not by much. Here is some data collected using HTTP requests via ApacheBench using `-n 200 -c 20`:\n```\nnginx:\n\n    Requests per second:    754.53 [#/sec] (mean)\n    Time per request:       26.507 [ms] (mean)\n    Time per request:       1.325 [ms] (mean, across all concurrent requests)\n\nconnectable:\n\n    Requests per second:    606.32 [#/sec] (mean)\n    Time per request:       32.986 [ms] (mean)\n    Time per request:       1.649 [ms] (mean, across all concurrent requests)\n```\nMemory overhead is also roughly comparable per connection. Added network latency is near zero since it's running on the same host as clients. Keep in mind, Connectable is designed to run on each host for best performance and to avoid SPOF.\n\nAlthough Connectable is Good Enough for most cases, if the overhead is a deal breaker for a particular case, don't use it in that particular case. Alternatives include working with service registries directly, just using DNS discovery with known ports, setting up a full SDN, etc.\n\n## Modules\n\nTodo\n\n## Why not just DNS?\n\nIf you're using Consul DNS, SkyDNS, et al, you may wonder why Connectable is necessary. The answer is ports. Most software is not designed for dynamic ports. Most software can only resolve hostnames to IPs. You have to hard configure the port used.\n\nIf you are able to run all containers publishing exposed ports on known ports (`-p 80:80`), you might not need Connectable. If you have a fancy SDN solution that makes private container IPs publicly addressable and they use known ports, you don't need Connectable.\n\nHowever, if you run containers with non-conventional ports, or don't have control over published ports, or just want to not care and wish it were magically taken care of ... that's what Connectable is for.\n\nConnectable when combined with Registrator lets you run containers with `-P` and not care about what port they publish as.\n\nAlso, DNS may not randomize results, effectively balancing services. Connectable ensures internal load balancing.\n\n## Notes\n\nhttps://github.com/docker/docker/issues/7468\nhttps://github.com/docker/docker/issues/7467\n\n## Sponsor and Thanks\n\nConnectable is sponsored by [Weave](http://weave.works). The original ambassadord proof of concept was made possible thanks to [DigitalOcean](http://digitalocean.com). Also thanks to [Jérôme Petazzoni](https://github.com/jpetazzo) for helping with the iptables bits that make this magical.\n\n## License\n\nMIT\n<img src=\"https://ga-beacon.appspot.com/UA-58928488-2/connectable/readme?pixel\" />\n"
  },
  {
    "path": "SPONSORS",
    "content": "DigitalOcean \thttp://digitalocean.com\nWeave         http://weave.works\n"
  },
  {
    "path": "VERSION",
    "content": "0.1.0\n"
  },
  {
    "path": "connectable.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\n\tenv \"github.com/MattAitchison/envconfig\"\n\t\"github.com/fsouza/go-dockerclient\"\n\t\"github.com/gliderlabs/connectable/pkg/lookup\"\n\n\t_ \"github.com/gliderlabs/connectable/pkg/lookup/dns\"\n)\n\nvar Version string\n\nvar (\n\tendpoint = env.String(\"docker_host\", \"unix:///var/run/docker.sock\", \"docker endpoint\")\n\tport     = env.String(\"port\", \"10000\", \"primary listen port\")\n\n\tself *docker.Container\n)\n\nfunc assert(err error) {\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc runNetCmd(container, image string, cmd string) error {\n\tclient, err := docker.NewClient(endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc, err := client.CreateContainer(docker.CreateContainerOptions{\n\t\tConfig: &docker.Config{\n\t\t\tImage:      image,\n\t\t\tCmd:        []string{cmd},\n\t\t\tEntrypoint: []string{\"/bin/sh\", \"-c\"},\n\t\t},\n\t\tHostConfig: &docker.HostConfig{\n\t\t\tPrivileged:  true,\n\t\t\tNetworkMode: fmt.Sprintf(\"container:%s\", container),\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := client.StartContainer(c.ID, nil); err != nil {\n\t\treturn err\n\t}\n\tstatus, err := client.WaitContainer(c.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif status != 0 {\n\t\treturn fmt.Errorf(\"netcmd non-zero exit: %v\", status)\n\t}\n\treturn client.RemoveContainer(docker.RemoveContainerOptions{\n\t\tID:    c.ID,\n\t\tForce: true,\n\t})\n}\n\nfunc originalDestinationPort(conn net.Conn) (string, error) {\n\tf, err := conn.(*net.TCPConn).File()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\taddr, err := syscall.GetsockoptIPv6Mreq(\n\t\tint(f.Fd()), syscall.IPPROTO_IP, 80) // 80 = SO_ORIGINAL_DST\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tport := uint16(addr.Multiaddr[2])<<8 + uint16(addr.Multiaddr[3])\n\treturn strconv.Itoa(int(port)), nil\n}\n\nfunc inspectBackend(sourceIP, destPort string) (string, error) {\n\tclient, err := docker.NewClient(endpoint)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tlabel := fmt.Sprintf(\"connect.%s\", destPort)\n\n\t// todo: cache, invalidate with container destroy events\n\tcontainers, err := client.ListContainers(docker.ListContainersOptions{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, listing := range containers {\n\t\tcontainer, err := client.InspectContainer(listing.ID)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif container.NetworkSettings.IPAddress == sourceIP {\n\t\t\tbackend, ok := container.Config.Labels[label]\n\t\t\tif !ok {\n\t\t\t\treturn \"\", fmt.Errorf(\"connect label '%s' not found: %v\", label, container.Config.Labels)\n\t\t\t}\n\t\t\treturn backend, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"unable to find container with source IP\")\n}\n\nfunc lookupBackend(conn net.Conn) string {\n\tsourceIP, _, _ := net.SplitHostPort(conn.RemoteAddr().String())\n\tdestPort, err := originalDestinationPort(conn)\n\tif err != nil {\n\t\tlog.Println(\"unable to determine destination port\")\n\t\treturn \"\"\n\t}\n\n\tbackend, err := inspectBackend(sourceIP, destPort)\n\tif err != nil {\n\t\tlog.Println(err)\n\t\treturn \"\"\n\t}\n\treturn backend\n}\n\nfunc proxyConn(conn net.Conn, addr string) {\n\tbackend, err := net.Dial(\"tcp\", addr)\n\tdefer conn.Close()\n\tif err != nil {\n\t\tlog.Println(\"proxy\", err.Error())\n\t\treturn\n\t}\n\tdefer backend.Close()\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tio.Copy(backend, conn)\n\t\tbackend.(*net.TCPConn).CloseWrite()\n\t\tclose(done)\n\t}()\n\tio.Copy(conn, backend)\n\tconn.(*net.TCPConn).CloseWrite()\n\t<-done\n}\n\nfunc setupContainer(id string) error {\n\tre := regexp.MustCompile(\"connect\\\\.(\\\\d+)\")\n\tclient, err := docker.NewClient(endpoint)\n\tif err != nil {\n\t\tlog.Println(err)\n\t\treturn err\n\t}\n\tcontainer, err := client.InspectContainer(id)\n\tif err != nil {\n\t\tlog.Println(err)\n\t\treturn err\n\t}\n\tif container.HostConfig.NetworkMode == \"bridge\" || container.HostConfig.NetworkMode == \"default\" {\n\t\thasBackends := false\n\t\tcmds := []string{\n\t\t\t\"/sbin/sysctl -w net.ipv4.conf.all.route_localnet=1\",\n\t\t\t\"iptables -t nat -I POSTROUTING 1 -m addrtype --src-type LOCAL --dst-type UNICAST -j MASQUERADE\",\n\t\t}\n\t\tfor k, _ := range container.Config.Labels {\n\t\t\tresults := re.FindStringSubmatch(k)\n\t\t\tif len(results) > 1 {\n\t\t\t\thasBackends = true\n\t\t\t\tcmds = append(cmds, fmt.Sprintf(\n\t\t\t\t\t\"iptables -t nat -I OUTPUT 1 -m addrtype --src-type LOCAL --dst-type LOCAL -p tcp --dport %s -j DNAT --to-destination %s:%s\",\n\t\t\t\t\tresults[1], self.NetworkSettings.IPAddress, results[1]))\n\t\t\t}\n\t\t}\n\t\tif hasBackends {\n\t\t\tlog.Printf(\"setting iptables on %s \\n\", container.ID[:12])\n\t\t\tshellCmd := strings.Join(cmds, \" && \")\n\t\t\terr := runNetCmd(container.ID, self.Image, shellCmd)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"error setting iptables on %s: %s \\n\", container.ID[:12], err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n\n}\n\nfunc monitorContainers() {\n\tclient, err := docker.NewClient(endpoint)\n\tassert(err)\n\tevents := make(chan *docker.APIEvents)\n\tassert(client.AddEventListener(events))\n\tlist, _ := client.ListContainers(docker.ListContainersOptions{})\n\tfor _, listing := range list {\n\t\tgo setupContainer(listing.ID)\n\t}\n\tfor msg := range events {\n\t\tswitch msg.Status {\n\t\tcase \"start\":\n\t\t\tgo setupContainer(msg.ID)\n\t\t}\n\t}\n}\n\nfunc main() {\n\tlistener, err := net.Listen(\"tcp\", \":\"+port)\n\tassert(err)\n\n\tfmt.Printf(\"# Connectable %s listening on %s ...\\n\", Version, port)\n\n\tclient, err := docker.NewClient(endpoint)\n\tassert(err)\n\n\tselfImageRe := regexp.MustCompile(\"(?:^|/)connectable(?:$|:)\")\n\n\tlist, err := client.ListContainers(docker.ListContainersOptions{})\n\tassert(err)\n\tfor _, listing := range list {\n\t\tc, err := client.InspectContainer(listing.ID)\n\t\tassert(err)\n\t\tif c.Config.Hostname == os.Getenv(\"HOSTNAME\") && selfImageRe.FindString(c.Config.Image) != \"\" {\n\t\t\tself = c\n\t\t\tif c.HostConfig.NetworkMode == \"bridge\" || c.HostConfig.NetworkMode == \"default\" {\n\t\t\t\tfmt.Printf(\"# Setting iptables on connectable... \")\n\t\t\t\tshellCmd := fmt.Sprintf(\"iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports %s\", port)\n\t\t\t\tassert(runNetCmd(c.ID, c.Image, shellCmd))\n\t\t\t\tfmt.Printf(\"done.\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif self == nil {\n\t\tfmt.Println(\"# unable to find self\")\n\t\tos.Exit(1)\n\t}\n\n\tgo monitorContainers()\n\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tbackend := lookupBackend(conn)\n\t\tif backend == \"\" {\n\t\t\tconn.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tbackendAddrs, err := lookup.Resolve(backend)\n\t\tif err != nil {\n\t\t\tlog.Println(err)\n\t\t\tconn.Close()\n\t\t\tcontinue\n\t\t}\n\t\tif len(backendAddrs) == 0 {\n\t\t\tlog.Println(conn.RemoteAddr(), backend, \"no backends\")\n\t\t\tconn.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Println(conn.RemoteAddr(), backend, \"->\", backendAddrs[0])\n\t\tgo proxyConn(conn, backendAddrs[0])\n\t}\n}\n"
  },
  {
    "path": "pkg/lookup/cache.go",
    "content": "package lookup\n\nimport (\n\t\"time\"\n\n\t\"github.com/youtube/vitess/go/cache\"\n)\n\nconst (\n\tcacheCapacity = 1024 * 1024 // 1MB\n\tcacheTTL      = 1           // 1 second\n)\n\nvar (\n\tresolveCache = cache.NewLRUCache(cacheCapacity)\n)\n\ntype cacheValue struct {\n\tValue     []string\n\tCreatedAt int64\n}\n\nfunc (cv *cacheValue) Size() int {\n\tvar size int\n\tfor _, s := range cv.Value {\n\t\tsize += len(s)\n\t}\n\treturn size\n}\n\nfunc (cv *cacheValue) Expired() bool {\n\treturn (time.Now().Unix() - cv.CreatedAt) > cacheTTL\n}\n"
  },
  {
    "path": "pkg/lookup/consulkv/consulkv.go",
    "content": "package consulkv\n\n// TODO\n"
  },
  {
    "path": "pkg/lookup/dns/dns.go",
    "content": "package dns\n\nimport (\n\t\"log\"\n\t\"net\"\n\t\"strconv\"\n\n\t\"github.com/gliderlabs/connectable/pkg/lookup\"\n\t\"github.com/miekg/dns\"\n)\n\nvar (\n\tconfig *dns.ClientConfig\n)\n\nfunc init() {\n\tlookup.Register(\"dns\", new(dnsResolver))\n\tvar err error\n\tconfig, err = dns.ClientConfigFromFile(\"/etc/resolv.conf\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\ntype dnsResolver struct{}\n\nfunc (r *dnsResolver) Lookup(addr string) ([]string, error) {\n\tquery := new(dns.Msg)\n\tquery.SetQuestion(dns.Fqdn(addr), dns.TypeSRV)\n\tquery.RecursionDesired = false\n\tclient := new(dns.Client)\n\tresp, _, err := client.Exchange(query, net.JoinHostPort(config.Servers[0], config.Port))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(resp.Answer) == 0 {\n\t\treturn []string{}, nil\n\t}\n\tvar addrs []string\n\tfor i, record := range resp.Answer {\n\t\tport := strconv.Itoa(int(record.(*dns.SRV).Port))\n\t\tip := record.(*dns.SRV).Target\n\t\tif len(resp.Extra) >= i+1 {\n\t\t\tip = resp.Extra[i].(*dns.A).A.String()\n\t\t}\n\t\taddrs = append(addrs, net.JoinHostPort(ip, port))\n\t}\n\treturn addrs, nil\n}\n"
  },
  {
    "path": "pkg/lookup/etcd/etcd.go",
    "content": "package etcd\n\n// TODO\n"
  },
  {
    "path": "pkg/lookup/lookup.go",
    "content": "package lookup\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\tenv \"github.com/MattAitchison/envconfig\"\n)\n\nvar (\n\tresolverName = env.String(\"lookup_resolver\", \"dns\", \"resolver to use for lookups\")\n\tdebugMode    = env.Bool(\"lookup_debug\", false, \"enable debug output\")\n\tresolvers    = make(map[string]Resolver)\n)\n\nfunc debug(v ...interface{}) {\n\tif debugMode {\n\t\tlog.Println(v...)\n\t}\n}\n\ntype Resolver interface {\n\tLookup(addr string) ([]string, error)\n}\n\nfunc Register(name string, resolver Resolver) {\n\tresolvers[name] = resolver\n}\n\nfunc Resolve(addr string) ([]string, error) {\n\tcached, ok := resolveCache.Get(addr)\n\tif ok && !cached.(*cacheValue).Expired() {\n\t\tdebug(\"lookup: resolving [cache]:\", addr, cached.(*cacheValue).Value)\n\t\treturn cached.(*cacheValue).Value, nil\n\t}\n\tresolver, ok := resolvers[resolverName]\n\tif !ok {\n\t\tdebug(\"lookup: resolver not found:\", resolverName)\n\t\treturn []string{}, fmt.Errorf(\"resolver not found: %s\", resolverName)\n\t}\n\tvalue, err := resolver.Lookup(addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresolveCache.Set(addr, &cacheValue{value, time.Now().Unix()})\n\tdebug(\"lookup: resolving:\", addr, value)\n\treturn value, nil\n}\n"
  },
  {
    "path": "pkg/lookup/ports.go",
    "content": "package lookup\n\nvar namedPorts = map[string]string{\n\t\"syslog\":      \"514\",\n\t\"http\":        \"80\",\n\t\"https\":       \"443\",\n\t\"ssh\":         \"22\",\n\t\"consul\":      \"8500\",\n\t\"consul-http\": \"8500\",\n\t\"etcd\":        \"2379\",\n\t\"dns\":         \"53\",\n}\n"
  },
  {
    "path": "run",
    "content": "#!/bin/sh\nset -e\n\n: \"${DEV_IMAGE_TAG:=connectable:dev-env}\"\n: \"${DEV_CONTAINER_NAME:=connectable-dev}\"\n\nexists() { # type name\n  docker inspect --type \"$1\" -f '{{.Id}}' \"$2\" >/dev/null 2>&1\n}\n\nif ! exists image \"$DEV_IMAGE_TAG\"; then\n  printf '\\n==> %s\\n\\n' 'Building base image...'\n  docker build -t \"$DEV_IMAGE_TAG\" -f Dockerfile.dev .\nfi\n\nif ! exists container \"$DEV_CONTAINER_NAME\"; then\n  docker create >/dev/null -it --name \"$DEV_CONTAINER_NAME\" \\\n    -h dev \\\n    -e GOPATH=/go \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -v \"$(pwd)\":/go/src/github.com/gliderlabs/connectable \\\n    -w /go/src/github.com/gliderlabs/connectable \\\n    \"$DEV_IMAGE_TAG\" sh -c '\n      run() { cmd=$1; shift; printf \"\\\\n==> %s\\\\n\\\\n\" \"$cmd $*\"; \"$cmd\" \"$@\"; }\n      set -e\n      run go get\n      run go build -ldflags \"-X main.Version dev\" -o /bin/connectable\n      run exec /bin/connectable'\nfi\n\nexec docker start -ia \"$DEV_CONTAINER_NAME\"\n"
  }
]