├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── contrib └── nginx.conf ├── docker └── client.go ├── main.go ├── main_test.go ├── plugins.go ├── plugins ├── containerEnv.js └── default.js ├── skydns.go └── utils ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | skydock 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | 6 | install: 7 | - go get github.com/skynetservices/skydns1/msg 8 | - go get github.com/skynetservices/skydns1/client 9 | - go get github.com/crosbymichael/skydock/docker 10 | - go get github.com/crosbymichael/skydock/utils 11 | - go get github.com/influxdb/influxdb/client 12 | - go get github.com/crosbymichael/log 13 | - go get github.com/robertkrimen/otto 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM crosbymichael/golang 2 | 3 | # go get to download all the deps 4 | RUN go get -u github.com/crosbymichael/skydock 5 | 6 | ADD . /go/src/github.com/crosbymichael/skydock 7 | ADD plugins/ /plugins 8 | 9 | RUN cd /go/src/github.com/crosbymichael/skydock && go install . ./... 10 | 11 | ENTRYPOINT ["/go/bin/skydock"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Michael Crosby. michael@crosbymichael.com 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, 7 | modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, 20 | DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH 24 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Skydock - Automagic Service Discovery for [Docker](https://github.com/dotcloud/docker) 2 | [![Build Status](https://travis-ci.org/crosbymichael/skydock.png)](https://travis-ci.org/crosbymichael/skydock) 3 | 4 | 5 | ## NOTICE 6 | 7 | Docker supports DNS based service discovery now. You should use the Docker implementation instead of this project. 8 | Skydock was built at a time when Docker did not support DNS discovery or auto registration. I'll keep the repo 9 | up for past years and as reference for others but don't use it if you have a recent version of Docker. 10 | 11 | 12 | Skydock monitors docker events when containers start, stop, die, kill, etc and inserts records into a dynamic 13 | DNS server [skydns](https://github.com/skynetservices/skydns1). This allows standard DNS queries for services 14 | running inside docker containers. Because lets face it, if you have to modify your application code to work 15 | with other service discovery solutions you might as well just give up. DNS just works and it works well. 16 | Also you cannot be expected to modify application code that you don't own. Passing service urls via the 17 | cli or in static config files (nginx) will not be possible if your service discovery solution requires 18 | a client library just to fetch an IP. 19 | 20 | 21 | [Skydns](https://github.com/skynetservices/skydns1) is a very small and simple server that does DNS for 22 | discovery very well. The authors and contributors to skydns helped a lot to make this project possible. 23 | Skydns exposes a very simple REST API to add, update, and remove services. 24 | 25 | 26 | #### The Details 27 | 28 | When you start a container with docker an event is sent to skydock via the events endpoint. Skydock will 29 | then inspect the container's information and add an entry to skydns. We setup skydns to bind it's nameserver 30 | to the **docker0** bridge so that it is available to containers. For DNS queries that are not part of the 31 | domain registered with skydns for service discovery, skydns will forward the query to an authoritative nameserver. 32 | Skydns will return A, AAAA, and SRV records for registered services. 33 | 34 | 35 | 36 | When designing skydock I made the assumption that when in the context of service discovery a client does 37 | not care about a specific instance of a service running inside a container. The client cares about the 38 | service and in the context of docker a service is defined by an image. We have many images; redis, postgres, 39 | our frontend applications, queues, and workers. The URL scheme is designed using this assumption. 40 | 41 | 42 | **Parts of the URL** 43 | * Domain (domain name to resolve DNS requests for) 44 | * Environment (context of what type of service is running dev, production, qa, uat) 45 | * Service (the actual service name derived from the image name minus the repository crosbymichael/redis -> redis) 46 | * Instance (container's name representing the actual instance of a service) 47 | * Region (currently not used but will be your docker host, digitalocean, ec2; this will be used for multihost) 48 | 49 | 50 | A typical query will look like this if your domain is `crosbymichael.com` and environment is `production`: 51 | 52 | ```bash 53 | curl webapp.production.crosbymichael.com 54 | ``` 55 | 56 | The query above will return the IP for a container running the image webapp. If we want a specific instance 57 | we can prepend the container name to the query. If our webapp container's name was webapp1 we could do this 58 | to get the specific container. 59 | 60 | ```bash 61 | curl webapp1.webapp.production.crosbymichael.com 62 | ``` 63 | 64 | Very simple and easy and no client code was harmed in this demonstration. Wildcard queries are also supported. 65 | 66 | ```bash 67 | dig @172.17.42.1 "webapp.*.crosbymichael.com" 68 | ``` 69 | 70 | 71 | #### Setup 72 | 73 | So what type of hacks do you have to do to get this running? Nothing, everything runs inside docker containers. 74 | You are just two `docker run` s away from bliss. 75 | 76 | 77 | Ok, I lied. You have to make one change to your docker daemon. You need to run the daemon with the `-dns` flag so 78 | that docker injects a specific nameserver into the `/etc/resolv.conf` of each container that is run. First get the 79 | IP address of the `docker0` bridge. We will need to know the IP of the bridge so that we can tell skydns to bind it's 80 | nameserver to that IP. For this example we will use the ip `172.17.42.1` as the `docker0` bridge. 81 | 82 | 83 | ```bash 84 | # start your daemon with the -dns flag, figure it out... 85 | docker -d --bip=172.17.42.1/16 --dns=172.17.42.1 # + what other settings you use 86 | ``` 87 | 88 | **Note:** 89 | You can also pass the `-dns` flag to individual containers so that the DNS options only apply to specific 90 | containers and not everything started by the daemon. But what fun is that? 91 | 92 | Now we need to start skydns before our other containers are run or else they will not be able to resolve DNS queries. 93 | 94 | ```bash 95 | docker pull crosbymichael/skydns 96 | docker run -d -p 172.17.42.1:53:53/udp --name skydns crosbymichael/skydns -nameserver 8.8.8.8:53 -domain docker 97 | ``` 98 | 99 | We add the name skydns to the container and we use `-p` and tell docker to bind skydns port 53/udp to the docker0 bridge's IP. 100 | We give skydns a nameserver of `8.8.8.8:53`. This nameserver is used to forward queries that are not for service discovery to a 101 | real nameserver. If you don't know `8.8.8.8` is google's public DNS address. 102 | 103 | 104 | Next is the `-domain` flag. This is the domain that you want skydns to resolve DNS queries for. In this example I am running 105 | docker on my local development machine so I am using the domain name `docker`. Any requests for services `*.docker` will be 106 | resolved by skydns for service discovery, all other requests will be forwarded to `8.8.8.8`. 107 | 108 | 109 | Now that skydns is running we can start skydock to bridge the gap between docker and skydns. 110 | 111 | 112 | ```bash 113 | docker pull crosbymichael/skydock 114 | docker run -d -v /var/run/docker.sock:/docker.sock --name skydock crosbymichael/skydock -ttl 30 -environment dev -s /docker.sock -domain docker -name skydns 115 | ``` 116 | 117 | 118 | This one is as little more involved but the parts are still simple. First we give it the name of skydock and we bind docker's unix socket into the container. 119 | I'm guessing for most, you do not want to service the docker API on a tcp port for containers to reach. If we bind the unix socket into this container we don't 120 | have to worry about other containers accessing the API, only skydock. We also add a link for skydns so that skydock knows where to make requests to insert 121 | new records. We are pre DNS discovery at this point. 122 | 123 | 124 | Now we have a few settings to assign to skydock. First is the TTL value that you want all services to have when skydock adds them to DNS. I'm using a 125 | TTL value 30 seconds but you can set it higher or lower if needed. Skydock will also start a heartbeat for the service after it is added. You can use 126 | the `-beat` flag to set this default interval in seconds for the heartbeat or skydock will set the heartbeat interval to `TTL -(TTL/4)`. I know, too 127 | complicated. 128 | 129 | 130 | Next is the `-environment` flag which is the second part of your DNS queries. I set this to `dev` because it is running on my local machine. `-s` is 131 | the final option and it just tells skydock where to find docker's unix socket so that it can make requests to docker's API. 132 | 133 | 134 | Now you're done. Just start containers and use intuitive urls to discover your services. Here is an small example starting a redis server and connecting 135 | the redis-cli to that instance of the service. Because it's DNS you can specific the urls on `docker run`. 136 | 137 | 138 | ```bash 139 | # run an instance of redis 140 | docker run -d --name redis1 crosbymichael/redis 141 | 03582c0de0ebb10665678d6ed530ae98bebd7d63dad5e7fb1cd53ffb1f85d91d 142 | 143 | # run the cli and connect to our new instance 144 | docker run -t -i crosbymichael/redis-cli -h redis1.redis.dev.docker 145 | 146 | redis.dev.docker:6379> set name koye 147 | OK 148 | redis.dev.docker:6379> get name 149 | "koye" 150 | redis.dev.docker:6379> 151 | 152 | ``` 153 | 154 | That is, the `redis1` named `crosbymichael/redis` container was available under the hostname `redis1.redis.dev.docker`. 155 | 156 | ``` 157 | dig @172.17.42.1 +short redis1.redis.dev.docker 158 | 172.17.0.4 159 | ``` 160 | 161 | If you were to run additional `crosbymichael/redis` containers, they would all be available under the `redis.dev.docker` hostname. 162 | 163 | ``` 164 | docker run -d --name redis2 crosbymichael/redis 165 | docker run -d --name redis3 crosbymichael/redis 166 | 167 | dig @172.17.42.1 +short redis.dev.docker 168 | 172.17.0.4 169 | 172.17.0.5 170 | 172.17.0.6 171 | ``` 172 | 173 | #### Plugin support 174 | I just added plugin support via [otto](https://github.com/robertkrimen/otto) to allow users to write plugins in javascript. Currently only one function uses plugins and that is `createService(container)`. This function takes a container's configuration and converts it into a DNS service The current functionality is implementing in this javascript function: 175 | 176 | ```javascript 177 | function createService(container) { 178 | return { 179 | Port: 80, 180 | Environment: defaultEnvironment, 181 | TTL: defaultTTL, 182 | Service: cleanImageName(container.Image), 183 | Instance: removeSlash(container.Name), 184 | Host: container.NetworkSettings.IpAddress 185 | }; 186 | } 187 | ``` 188 | 189 | Your function must be called `createservice` which takes one object, the container, and must return a service with the fields shown above. In your plugin you have access to the following global variables and functions. 190 | 191 | 192 | ```javascript 193 | var defaultEnvironment = "string - the environment from the -environment flag"; 194 | var defaultTTL = 30; // int - the ttl value from the -ttl flag 195 | 196 | function cleanImageName(string) string // cleans the repo and tags of the passed parameter returning the result 197 | function removeSlash(string) string // removes all / from the passed parameter returning the result 198 | ``` 199 | 200 | And that is it. Just add a `createservice` function to a .js file then use the `-plugins` flag to enable your new plugin. Plugins are loaded at start so changes made to the functions during the life of skydock are not reflected, you have to restart ( done for performance ). 201 | 202 | ```bash 203 | docker run -d -v /var/run/docker.sock:/docker.sock -v /myplugins.js:/myplugins.js --name skydock --link skydns:skydns crosbymichael/skydock -s /docker.sock -domain docker -plugins /myplugins.js 204 | ``` 205 | 206 | Feel free to submit your plugins to this repo under the `plugins/` directory. 207 | 208 | 209 | #### TODO/ROADMAP 210 | * Multihost support 211 | * Handle multiple ports via SRV records 212 | 213 | #### Bugs 214 | * Please report all skydock bugs on this repository 215 | * Report all skydns bugs [here](https://github.com/skynetservices/skydns1/issues?state=open) 216 | 217 | #### License - MIT 218 | 219 | Copyright (c) 2014 Michael Crosby. michael@crosbymichael.com 220 | 221 | Permission is hereby granted, free of charge, to any person 222 | obtaining a copy of this software and associated documentation 223 | files (the "Software"), to deal in the Software without 224 | restriction, including without limitation the rights to use, copy, 225 | modify, merge, publish, distribute, sublicense, and/or sell copies 226 | of the Software, and to permit persons to whom the Software is 227 | furnished to do so, subject to the following conditions: 228 | 229 | The above copyright notice and this permission notice shall be 230 | included in all copies or substantial portions of the Software. 231 | 232 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 233 | EXPRESS OR IMPLIED, 234 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 235 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 236 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 237 | HOLDERS BE LIABLE FOR ANY CLAIM, 238 | DAMAGES OR OTHER LIABILITY, 239 | WHETHER IN AN ACTION OF CONTRACT, 240 | TORT OR OTHERWISE, 241 | ARISING FROM, OUT OF OR IN CONNECTION WITH 242 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 243 | -------------------------------------------------------------------------------- /contrib/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | # in order to use `docker logs` 4 | error_log stderr; 5 | 6 | events { 7 | worker_connections 1024; 8 | use epoll; 9 | } 10 | 11 | http { 12 | 13 | # tells nginx to use the skydns resolver 14 | resolver 172.17.42.1 valid=5s; 15 | resolver_timeout 5s; 16 | 17 | server { 18 | # your usual stuff 19 | listen 80; 20 | server_name kswizz.com *.kswizz.com; 21 | root /app; 22 | 23 | # so, nginx is stupid. we need to fool it into thinking 24 | # that the proxy upstream is a runtime variable (e.g. 25 | # it could be based off a $http_* variable.) this is the 26 | # only way that triggers to use the resolver at runtime. 27 | 28 | set $dns app-1.ruby.live.docker; 29 | 30 | 31 | # let's say we have two locations, the first of which 32 | # proxies to a web-app service 33 | 34 | location /api { 35 | rewrite ^/api/?(.*) /$1 break; 36 | 37 | # and here, we simply use our previously defined variable. 38 | proxy_pass http://$dns:80; 39 | } 40 | 41 | location / { 42 | try_files $uri /index.html; 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /docker/client.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "net/http/httputil" 11 | "os" 12 | "os/signal" 13 | "syscall" 14 | 15 | "github.com/crosbymichael/log" 16 | "github.com/crosbymichael/skydock/utils" 17 | ) 18 | 19 | type ( 20 | Docker interface { 21 | FetchAllContainers() ([]*Container, error) 22 | FetchContainer(name, image string) (*Container, error) 23 | GetEvents() chan *Event 24 | } 25 | 26 | Event struct { 27 | ContainerId string `json:"id"` 28 | Status string `json:"status"` 29 | Image string `json:"from"` 30 | } 31 | 32 | ContainerConfig struct { 33 | Hostname string 34 | Image string 35 | Env []string 36 | } 37 | 38 | Binding struct { 39 | HostIp string 40 | HostPort string 41 | } 42 | 43 | NetworkSettings struct { 44 | IpAddress string 45 | Ports map[string][]Binding 46 | } 47 | 48 | // GET /containers/json returns the state of the container, one of: 49 | // - created; 50 | // - restarting; 51 | // - running; 52 | // - paused; 53 | // - exitedor; 54 | // - dead; 55 | State string 56 | 57 | Container struct { 58 | Id string 59 | Image string 60 | Name string 61 | Config *ContainerConfig 62 | NetworkSettings *NetworkSettings 63 | State State 64 | } 65 | 66 | dockerClient struct { 67 | path string 68 | } 69 | ) 70 | 71 | var ( 72 | ErrImageNotTagged = errors.New("image not tagged") 73 | ) 74 | 75 | func NewClient(path string) (Docker, error) { 76 | return &dockerClient{path}, nil 77 | } 78 | 79 | func (d *dockerClient) newConn() (*httputil.ClientConn, error) { 80 | prot, path := utils.SplitURI(d.path) 81 | conn, err := net.Dial(prot, path) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return httputil.NewClientConn(conn, nil), nil 86 | } 87 | 88 | func (d *dockerClient) FetchContainer(name, image string) (*Container, error) { 89 | c, err := d.newConn() 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer c.Close() 94 | 95 | req, err := http.NewRequest("GET", fmt.Sprintf("/containers/%s/json", name), nil) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | resp, err := c.Do(req) 101 | if err != nil { 102 | return nil, err 103 | } 104 | defer resp.Body.Close() 105 | 106 | if resp.StatusCode == http.StatusOK { 107 | var ( 108 | container *Container 109 | d = json.NewDecoder(resp.Body) 110 | ) 111 | 112 | if err = d.Decode(&container); err != nil { 113 | return nil, err 114 | } 115 | 116 | // These should match or else it's from an image that is not tagged 117 | if image != "" && utils.RemoveTag(image) != utils.RemoveTag(container.Config.Image) { 118 | return nil, ErrImageNotTagged 119 | } 120 | container.Image = image 121 | 122 | return container, nil 123 | } 124 | return nil, fmt.Errorf("Could not fetch container %d", resp.StatusCode) 125 | } 126 | 127 | func (d *dockerClient) FetchAllContainers() ([]*Container, error) { 128 | req, err := http.NewRequest("GET", fmt.Sprintf("/containers/json"), nil) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | c, err := d.newConn() 134 | if err != nil { 135 | return nil, err 136 | } 137 | defer c.Close() 138 | 139 | resp, err := c.Do(req) 140 | if err != nil { 141 | return nil, err 142 | } 143 | defer resp.Body.Close() 144 | 145 | if resp.StatusCode == http.StatusOK { 146 | var containers []*Container 147 | if err = json.NewDecoder(resp.Body).Decode(&containers); err != nil { 148 | return nil, err 149 | } 150 | return containers, nil 151 | } 152 | return nil, fmt.Errorf("invalid HTTP request %d %s", resp.StatusCode, resp.Status) 153 | } 154 | 155 | func (d *dockerClient) GetEvents() chan *Event { 156 | eventChan := make(chan *Event, 100) // 100 event buffer 157 | go func() { 158 | defer close(eventChan) 159 | 160 | c, err := d.newConn() 161 | if err != nil { 162 | log.Logf(log.FATAL, "cannot connect to docker: %s", err) 163 | return 164 | } 165 | defer c.Close() 166 | 167 | req, err := http.NewRequest("GET", "/events", nil) 168 | if err != nil { 169 | log.Logf(log.ERROR, "bad request for events: %s", err) 170 | return 171 | } 172 | 173 | resp, err := c.Do(req) 174 | if err != nil { 175 | log.Logf(log.FATAL, "cannot connect to events endpoint: %s", err) 176 | return 177 | } 178 | defer resp.Body.Close() 179 | 180 | // handle signals to stop the socket 181 | sigChan := make(chan os.Signal, 1) 182 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) 183 | go func() { 184 | for sig := range sigChan { 185 | log.Logf(log.INFO, "received signal '%v', exiting", sig) 186 | 187 | c.Close() 188 | close(eventChan) 189 | os.Exit(0) 190 | } 191 | }() 192 | 193 | dec := json.NewDecoder(resp.Body) 194 | for { 195 | var event *Event 196 | if err := dec.Decode(&event); err != nil { 197 | if err == io.EOF { 198 | break 199 | } 200 | log.Logf(log.ERROR, "cannot decode json: %s", err) 201 | continue 202 | } 203 | eventChan <- event 204 | } 205 | log.Logf(log.DEBUG, "closing event channel") 206 | }() 207 | return eventChan 208 | } 209 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Multihost 3 | Multiple ports 4 | */ 5 | 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "os" 12 | "sync" 13 | "time" 14 | 15 | "github.com/crosbymichael/log" 16 | "github.com/crosbymichael/skydock/docker" 17 | "github.com/crosbymichael/skydock/utils" 18 | influxdb "github.com/influxdb/influxdb/client" 19 | "github.com/skynetservices/skydns1/client" 20 | "github.com/skynetservices/skydns1/msg" 21 | ) 22 | 23 | var ( 24 | pathToSocket string 25 | domain string 26 | environment string 27 | skydnsUrl string 28 | skydnsContainerName string 29 | secret string 30 | ttl int 31 | beat int 32 | numberOfHandlers int 33 | pluginFile string 34 | 35 | skydns Skydns 36 | dockerClient docker.Docker 37 | plugins *pluginRuntime 38 | running = make(map[string]struct{}) 39 | runningLock = sync.Mutex{} 40 | ) 41 | 42 | func init() { 43 | flag.StringVar(&pathToSocket, "s", "/var/run/docker.sock", "path to the docker unix socket") 44 | flag.StringVar(&skydnsUrl, "skydns", "", "url to the skydns url") 45 | flag.StringVar(&skydnsContainerName, "name", "", "name of skydns container") 46 | flag.StringVar(&secret, "secret", "", "skydns secret") 47 | flag.StringVar(&domain, "domain", "", "same domain passed to skydns") 48 | flag.StringVar(&environment, "environment", "dev", "environment name where service is running") 49 | flag.IntVar(&ttl, "ttl", 60, "default ttl to use when registering a service") 50 | flag.IntVar(&beat, "beat", 0, "heartbeat interval") 51 | flag.IntVar(&numberOfHandlers, "workers", 3, "number of concurrent workers") 52 | flag.StringVar(&pluginFile, "plugins", "/plugins/default.js", "file containing javascript plugins (plugins.js)") 53 | 54 | flag.Parse() 55 | } 56 | 57 | func validateSettings() { 58 | if beat < 1 { 59 | beat = ttl - (ttl / 4) 60 | } 61 | 62 | if (skydnsUrl != "") && (skydnsContainerName != "") { 63 | fatal(fmt.Errorf("specify 'name' or 'skydns', not both")) 64 | } 65 | 66 | if (skydnsUrl == "") && (skydnsContainerName == "") { 67 | skydnsUrl = "http://" + os.Getenv("SKYDNS_PORT_8080_TCP_ADDR") + ":8080" 68 | } 69 | 70 | if domain == "" { 71 | fatal(fmt.Errorf("Must specify your skydns domain")) 72 | } 73 | } 74 | 75 | func setupLogger() error { 76 | var ( 77 | logger log.Logger 78 | err error 79 | ) 80 | 81 | if host := os.Getenv("INFLUXDB_HOST"); host != "" { 82 | config := &influxdb.ClientConfig{ 83 | Host: host, 84 | Database: os.Getenv("INFLUXDB_DATABASE"), 85 | Username: os.Getenv("INFLUXDB_USER"), 86 | Password: os.Getenv("INFLUXDB_PASSWORD"), 87 | } 88 | 89 | logger, err = log.NewInfluxdbLogger(fmt.Sprintf("%s.%s", environment, domain), "skydock", config) 90 | if err != nil { 91 | return err 92 | } 93 | } else { 94 | logger = log.NewStandardLevelLogger("skydock") 95 | } 96 | 97 | if err := log.SetLogger(logger); err != nil { 98 | return err 99 | } 100 | return nil 101 | } 102 | 103 | func heartbeat(uuid string) { 104 | runningLock.Lock() 105 | if _, exists := running[uuid]; exists { 106 | runningLock.Unlock() 107 | return 108 | } 109 | running[uuid] = struct{}{} 110 | runningLock.Unlock() 111 | 112 | defer func() { 113 | runningLock.Lock() 114 | delete(running, uuid) 115 | runningLock.Unlock() 116 | }() 117 | 118 | var errorCount int 119 | for _ = range time.Tick(time.Duration(beat) * time.Second) { 120 | if errorCount > 10 { 121 | // if we encountered more than 10 errors just quit 122 | log.Logf(log.ERROR, "aborting heartbeat for %s after 10 errors", uuid) 123 | return 124 | } 125 | 126 | // don't fill logs if we have a low beat 127 | // may need to do something better here 128 | if beat >= 30 { 129 | log.Logf(log.INFO, "updating ttl for %s", uuid) 130 | } 131 | 132 | if err := updateService(uuid, ttl); err != nil { 133 | errorCount++ 134 | log.Logf(log.ERROR, "%s", err) 135 | break 136 | } 137 | } 138 | } 139 | 140 | // restoreContainers loads all running containers and inserts 141 | // them into skydns when skydock starts 142 | func restoreContainers() error { 143 | containers, err := dockerClient.FetchAllContainers() 144 | if err != nil { 145 | return err 146 | } 147 | 148 | var container *docker.Container 149 | for _, cnt := range containers { 150 | uuid := utils.Truncate(cnt.Id) 151 | if container, err = dockerClient.FetchContainer(uuid, cnt.Image); err != nil { 152 | if err != docker.ErrImageNotTagged { 153 | log.Logf(log.ERROR, "failed to fetch %s on restore: %s", cnt.Id, err) 154 | } 155 | continue 156 | } 157 | 158 | service, err := plugins.createService(container) 159 | if err != nil { 160 | // doing a fatal here because we cannot do much if the plugins 161 | // return an invalid service or error 162 | fatal(err) 163 | } 164 | if err := sendService(uuid, service); err != nil { 165 | log.Logf(log.ERROR, "failed to send %s to skydns on restore: %s", uuid, err) 166 | } 167 | } 168 | return nil 169 | } 170 | 171 | // sendService sends the uuid and service data to skydns 172 | func sendService(uuid string, service *msg.Service) error { 173 | log.Logf(log.INFO, "adding %s (%s) to skydns", uuid, service.Name) 174 | if err := skydns.Add(uuid, service); err != nil { 175 | // ignore erros for conflicting uuids and start the heartbeat again 176 | if err != client.ErrConflictingUUID { 177 | return err 178 | } 179 | log.Logf(log.INFO, "service already exists for %s. Resetting ttl.", uuid) 180 | updateService(uuid, ttl) 181 | } 182 | go heartbeat(uuid) 183 | return nil 184 | } 185 | 186 | func removeService(uuid string) error { 187 | log.Logf(log.INFO, "removing %s from skydns", uuid) 188 | return skydns.Delete(uuid) 189 | } 190 | 191 | func addService(uuid, image string) error { 192 | container, err := dockerClient.FetchContainer(uuid, image) 193 | if err != nil { 194 | if err != docker.ErrImageNotTagged { 195 | return err 196 | } 197 | return nil 198 | } 199 | 200 | service, err := plugins.createService(container) 201 | if err != nil { 202 | // doing a fatal here because we cannot do much if the plugins 203 | // return an invalid service or error 204 | fatal(err) 205 | } 206 | 207 | if err := sendService(uuid, service); err != nil { 208 | return err 209 | } 210 | return nil 211 | } 212 | 213 | func updateService(uuid string, ttl int) error { 214 | return skydns.Update(uuid, uint32(ttl)) 215 | } 216 | 217 | func eventHandler(c chan *docker.Event, group *sync.WaitGroup) { 218 | defer group.Done() 219 | 220 | for event := range c { 221 | log.Logf(log.DEBUG, "received event (%s) %s %s", event.Status, event.ContainerId, event.Image) 222 | uuid := utils.Truncate(event.ContainerId) 223 | 224 | switch event.Status { 225 | case "die", "stop", "kill": 226 | if err := removeService(uuid); err != nil { 227 | log.Logf(log.ERROR, "error removing %s from skydns: %s", uuid, err) 228 | } 229 | case "start", "restart": 230 | if err := addService(uuid, event.Image); err != nil { 231 | log.Logf(log.ERROR, "error adding %s to skydns: %s", uuid, err) 232 | } 233 | } 234 | } 235 | } 236 | 237 | func fatal(err error) { 238 | fmt.Fprintf(os.Stderr, "%s\n", err) 239 | os.Exit(1) 240 | 241 | } 242 | 243 | func main() { 244 | validateSettings() 245 | if err := setupLogger(); err != nil { 246 | fatal(err) 247 | } 248 | 249 | var ( 250 | err error 251 | group = &sync.WaitGroup{} 252 | ) 253 | 254 | plugins, err = newRuntime(pluginFile) 255 | if err != nil { 256 | fatal(err) 257 | } 258 | 259 | if dockerClient, err = docker.NewClient(pathToSocket); err != nil { 260 | log.Logf(log.FATAL, "error connecting to docker: %s", err) 261 | fatal(err) 262 | } 263 | 264 | if skydnsContainerName != "" { 265 | container, err := dockerClient.FetchContainer(skydnsContainerName, "") 266 | if err != nil { 267 | log.Logf(log.FATAL, "error retrieving skydns container '%s': %s", skydnsContainerName, err) 268 | fatal(err) 269 | } 270 | 271 | skydnsUrl = "http://" + container.NetworkSettings.IpAddress + ":8080" 272 | } 273 | 274 | log.Logf(log.INFO, "skydns URL: %s", skydnsUrl) 275 | 276 | if skydns, err = client.NewClient(skydnsUrl, secret, domain, "172.17.42.1:53"); err != nil { 277 | log.Logf(log.FATAL, "error connecting to skydns: %s", err) 278 | fatal(err) 279 | } 280 | 281 | log.Logf(log.DEBUG, "starting restore of containers") 282 | if err := restoreContainers(); err != nil { 283 | log.Logf(log.FATAL, "error restoring containers: %s", err) 284 | fatal(err) 285 | } 286 | 287 | events := dockerClient.GetEvents() 288 | 289 | group.Add(numberOfHandlers) 290 | // Start event handlers 291 | for i := 0; i < numberOfHandlers; i++ { 292 | go eventHandler(events, group) 293 | } 294 | 295 | log.Logf(log.DEBUG, "starting main process") 296 | group.Wait() 297 | log.Logf(log.DEBUG, "stopping cleanly via EOF") 298 | } 299 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/crosbymichael/skydock/docker" 10 | "github.com/skynetservices/skydns1/client" 11 | "github.com/skynetservices/skydns1/msg" 12 | ) 13 | 14 | type mockSkydns struct { 15 | services map[string]*msg.Service 16 | } 17 | 18 | func (s *mockSkydns) Add(uuid string, service *msg.Service) error { 19 | if _, exists := s.services[uuid]; exists { 20 | return client.ErrConflictingUUID 21 | } 22 | s.services[uuid] = service 23 | 24 | return nil 25 | } 26 | 27 | func (s *mockSkydns) Update(uuid string, ttl uint32) error { 28 | if _, exists := s.services[uuid]; !exists { 29 | return client.ErrServiceNotFound 30 | } 31 | s.services[uuid].TTL = ttl 32 | 33 | return nil 34 | } 35 | 36 | func (s *mockSkydns) Delete(uuid string) error { 37 | if _, exists := s.services[uuid]; !exists { 38 | return client.ErrServiceNotFound 39 | } 40 | delete(s.services, uuid) 41 | 42 | return nil 43 | } 44 | 45 | type mockDocker struct { 46 | containers map[string]*docker.Container 47 | } 48 | 49 | func (d *mockDocker) FetchContainer(name, image string) (*docker.Container, error) { 50 | if _, exists := d.containers[name]; !exists { 51 | return nil, fmt.Errorf("container not exists") 52 | } 53 | return d.containers[name], nil 54 | } 55 | 56 | func (d *mockDocker) FetchAllContainers() ([]*docker.Container, error) { 57 | out := make([]*docker.Container, len(d.containers)) 58 | 59 | i := 0 60 | for _, v := range d.containers { 61 | out[i] = v 62 | i++ 63 | } 64 | return out, nil 65 | } 66 | 67 | func (d *mockDocker) GetEvents() chan *docker.Event { 68 | return nil 69 | } 70 | 71 | func TestCreateService(t *testing.T) { 72 | environment = "production" 73 | ttl = 30 74 | 75 | p, err := newRuntime("plugins/default.js") 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | plugins = p 80 | 81 | container := &docker.Container{ 82 | Image: "crosbymichael/redis:latest", 83 | Name: "redis1", 84 | NetworkSettings: &docker.NetworkSettings{ 85 | IpAddress: "192.168.1.10", 86 | }, 87 | } 88 | 89 | service, err := p.createService(container) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | if service.Version != "redis1" { 95 | t.Fatalf("Expected version redis1 got %s", service.Version) 96 | } 97 | 98 | if service.Host != "192.168.1.10" { 99 | t.Fatalf("Expected host 192.168.1.10 got %s", service.Host) 100 | } 101 | 102 | if service.TTL != uint32(30) { 103 | t.Fatalf("Expected ttl 30 got %d", service.TTL) 104 | } 105 | 106 | if service.Environment != "production" { 107 | t.Fatalf("Expected environment production got %s", service.Environment) 108 | } 109 | 110 | if service.Name != "redis" { 111 | t.Fatalf("Expected name redis got %s", service.Name) 112 | } 113 | } 114 | 115 | func TestAddService(t *testing.T) { 116 | p, err := newRuntime("plugins/default.js") 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | plugins = p 121 | 122 | skydns = &mockSkydns{make(map[string]*msg.Service)} 123 | dockerClient = &mockDocker{ 124 | containers: map[string]*docker.Container{ 125 | "1": { 126 | Image: "crosbymichael/redis:latest", 127 | Name: "redis1", 128 | NetworkSettings: &docker.NetworkSettings{ 129 | IpAddress: "192.168.1.10", 130 | }, 131 | }, 132 | }, 133 | } 134 | 135 | if err := addService("1", "crosbymichael/redis"); err != nil { 136 | t.Fatal(err) 137 | } 138 | 139 | service := skydns.(*mockSkydns).services["1"] 140 | 141 | if service.Version != "redis1" { 142 | t.Fatalf("Expected version redis1 got %s", service.Version) 143 | } 144 | 145 | if service.Host != "192.168.1.10" { 146 | t.Fatalf("Expected host 192.168.1.10 got %s", service.Host) 147 | } 148 | 149 | if service.TTL != uint32(30) { 150 | t.Fatalf("Expected ttl 30 got %d", service.TTL) 151 | } 152 | 153 | if service.Environment != "production" { 154 | t.Fatalf("Expected environment production got %s", service.Environment) 155 | } 156 | 157 | if service.Name != "redis" { 158 | t.Fatalf("Expected name redis got %s", service.Name) 159 | } 160 | } 161 | 162 | func TestRemoveService(t *testing.T) { 163 | p, err := newRuntime("plugins/default.js") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | plugins = p 168 | 169 | skydns = &mockSkydns{make(map[string]*msg.Service)} 170 | dockerClient = &mockDocker{ 171 | containers: map[string]*docker.Container{ 172 | "1": { 173 | Image: "crosbymichael/redis:latest", 174 | Name: "redis1", 175 | NetworkSettings: &docker.NetworkSettings{ 176 | IpAddress: "192.168.1.10", 177 | }, 178 | }, 179 | }, 180 | } 181 | 182 | if err := addService("1", "crosbymichael/redis"); err != nil { 183 | t.Fatal(err) 184 | } 185 | 186 | service := skydns.(*mockSkydns).services["1"] 187 | 188 | if service == nil { 189 | t.Fatalf("Service not properly added") 190 | } 191 | 192 | if err := removeService("1"); err != nil { 193 | t.Fatal(err) 194 | } 195 | 196 | service = skydns.(*mockSkydns).services["1"] 197 | 198 | if service != nil { 199 | t.Fatalf("Service not properly removed") 200 | } 201 | } 202 | 203 | func TestEventHandler(t *testing.T) { 204 | var ( 205 | events = make(chan *docker.Event) 206 | group = &sync.WaitGroup{} 207 | ) 208 | 209 | p, err := newRuntime("plugins/default.js") 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | plugins = p 214 | 215 | skydns = &mockSkydns{make(map[string]*msg.Service)} 216 | container := &docker.Container{ 217 | Image: "crosbymichael/redis:latest", 218 | Name: "redis1", 219 | NetworkSettings: &docker.NetworkSettings{ 220 | IpAddress: "192.168.1.10", 221 | }, 222 | State: docker.State("running"), 223 | } 224 | 225 | dockerClient = &mockDocker{ 226 | containers: map[string]*docker.Container{ 227 | "3": container, 228 | }, 229 | } 230 | 231 | group.Add(1) 232 | go eventHandler(events, group) 233 | 234 | events <- &docker.Event{ 235 | Status: "start", 236 | Image: "crosbymichael/redis", 237 | ContainerId: "3", 238 | } 239 | 240 | close(events) 241 | time.Sleep(3 * time.Second) 242 | 243 | service := skydns.(*mockSkydns).services["3"] 244 | 245 | if service == nil { 246 | t.Fatal("No service added on event") 247 | } 248 | 249 | group.Wait() 250 | } 251 | 252 | func TestEnvironmentPlugin(t *testing.T) { 253 | environment = "production" 254 | ttl = 30 255 | 256 | p, err := newRuntime("plugins/containerEnv.js") 257 | if err != nil { 258 | t.Fatal(err) 259 | } 260 | plugins = p 261 | 262 | container := &docker.Container{ 263 | Image: "crosbymichael/redis:latest", 264 | Name: "redis1", 265 | NetworkSettings: &docker.NetworkSettings{ 266 | IpAddress: "192.168.1.10", 267 | }, 268 | Config: &docker.ContainerConfig{ 269 | Env: []string{ 270 | "DNS_SERVICE=rethinkdb", 271 | "DNS_ENVIRONMENT=test", 272 | "DNS_INSTANCE=test1", 273 | }, 274 | }, 275 | } 276 | 277 | service, err := p.createService(container) 278 | if err != nil { 279 | t.Fatal(err) 280 | } 281 | 282 | if service.Version != "test1" { 283 | t.Fatalf("Expected version test1 got %s", service.Version) 284 | } 285 | 286 | if service.Host != "192.168.1.10" { 287 | t.Fatalf("Expected host 192.168.1.10 got %s", service.Host) 288 | } 289 | 290 | if service.TTL != uint32(30) { 291 | t.Fatalf("Expected ttl 30 got %d", service.TTL) 292 | } 293 | 294 | if service.Environment != "test" { 295 | t.Fatalf("Expected environment test got %s", service.Environment) 296 | } 297 | 298 | if service.Name != "rethinkdb" { 299 | t.Fatalf("Expected name rethinkdb got %s", service.Name) 300 | } 301 | } 302 | 303 | func TestGetMappedPorts(t *testing.T) { 304 | p, err := newRuntime("plugins/default.js") 305 | if err != nil { 306 | t.Fatal(err) 307 | } 308 | plugins = p 309 | 310 | skydns = &mockSkydns{make(map[string]*msg.Service)} 311 | container := &docker.Container{ 312 | Image: "crosbymichael/redis:latest", 313 | Name: "redis1", 314 | NetworkSettings: &docker.NetworkSettings{ 315 | IpAddress: "192.168.1.10", 316 | Ports: map[string][]docker.Binding{ 317 | "53/udp": {{HostIp: "192.168.0.1", HostPort: "53"}}, 318 | }, 319 | }, 320 | State: docker.State("running"), 321 | } 322 | 323 | service, err := p.createService(container) 324 | if err != nil { 325 | t.Fatal(err) 326 | } 327 | if service.Port != 53 { 328 | t.Fatalf("Expected port 53 got %d", service.Port) 329 | } 330 | } 331 | 332 | func TestGetExposedPorts(t *testing.T) { 333 | p, err := newRuntime("plugins/default.js") 334 | if err != nil { 335 | t.Fatal(err) 336 | } 337 | plugins = p 338 | 339 | skydns = &mockSkydns{make(map[string]*msg.Service)} 340 | container := &docker.Container{ 341 | Image: "crosbymichael/redis:latest", 342 | Name: "redis1", 343 | NetworkSettings: &docker.NetworkSettings{ 344 | IpAddress: "192.168.1.10", 345 | Ports: map[string][]docker.Binding{ 346 | "6379/udp": nil, 347 | }, 348 | }, 349 | State: docker.State("running"), 350 | } 351 | 352 | service, err := p.createService(container) 353 | if err != nil { 354 | t.Fatal(err) 355 | } 356 | if service.Port != 6379 { 357 | t.Fatalf("Expected port 6379 got %d", service.Port) 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /plugins.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/crosbymichael/log" 8 | "github.com/crosbymichael/skydock/docker" 9 | "github.com/crosbymichael/skydock/utils" 10 | "github.com/robertkrimen/otto" 11 | "github.com/skynetservices/skydns1/msg" 12 | ) 13 | 14 | type pluginRuntime struct { 15 | o *otto.Otto 16 | } 17 | 18 | func (r *pluginRuntime) createService(container *docker.Container) (*msg.Service, error) { 19 | value, err := r.o.ToValue(*container) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | result, err := r.o.Call("createService", nil, value) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | if !result.IsObject() { 30 | return nil, fmt.Errorf("createService plugin did not return a valid object") 31 | } 32 | 33 | var ( 34 | obj = result.Object() 35 | service = &msg.Service{} 36 | ) 37 | 38 | rawTTL, err := getInt(obj, "TTL") 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | rawPort, err := getInt(obj, "Port") 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | if service.Name, err = getString(obj, "Service"); err != nil { 49 | return nil, err 50 | } 51 | if service.Version, err = getString(obj, "Instance"); err != nil { 52 | return nil, err 53 | } 54 | if service.Host, err = getString(obj, "Host"); err != nil { 55 | return nil, err 56 | } 57 | if service.Environment, err = getString(obj, "Environment"); err != nil { 58 | return nil, err 59 | } 60 | service.TTL = uint32(rawTTL) 61 | service.Port = uint16(rawPort) 62 | 63 | // I'm glad that is over 64 | return service, nil 65 | } 66 | 67 | func newRuntime(file string) (*pluginRuntime, error) { 68 | runtime := otto.New() 69 | log.Logf(log.INFO, "loading plugins from %s", file) 70 | 71 | content, err := ioutil.ReadFile(file) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | if _, err := runtime.Run(string(content)); err != nil { 77 | return nil, err 78 | } 79 | 80 | if err := loadDefaults(runtime); err != nil { 81 | return nil, err 82 | } 83 | return &pluginRuntime{runtime}, nil 84 | } 85 | 86 | func loadDefaults(runtime *otto.Otto) error { 87 | if err := runtime.Set("defaultTTL", ttl); err != nil { 88 | return err 89 | } 90 | if err := runtime.Set("defaultEnvironment", environment); err != nil { 91 | return err 92 | } 93 | if err := runtime.Set("cleanImageName", func(call otto.FunctionCall) otto.Value { 94 | name := call.Argument(0).String() 95 | result, _ := otto.ToValue(utils.CleanImageName(name)) 96 | return result 97 | }); err != nil { 98 | return err 99 | } 100 | if err := runtime.Set("removeSlash", func(call otto.FunctionCall) otto.Value { 101 | name := call.Argument(0).String() 102 | result, _ := otto.ToValue(utils.RemoveSlash(name)) 103 | return result 104 | }); err != nil { 105 | return err 106 | } 107 | return nil 108 | } 109 | 110 | // util functions 111 | 112 | func getString(obj *otto.Object, name string) (string, error) { 113 | v, err := obj.Get(name) 114 | if err != nil { 115 | return "", err 116 | } 117 | return v.ToString() 118 | } 119 | 120 | func getInt(obj *otto.Object, name string) (int64, error) { 121 | v, err := obj.Get(name) 122 | if err != nil { 123 | return -1, err 124 | } 125 | return v.ToInteger() 126 | } 127 | -------------------------------------------------------------------------------- /plugins/containerEnv.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // this plugin inspects a containers environment vars 4 | // to get service information 5 | function createService(container) { 6 | var env = createEnvironment(container); 7 | 8 | return { 9 | Port: 80, 10 | Environment: env.DNS_ENVIRONMENT || defaultEnvironment, 11 | TTL: env.DNS_TTL || defaultTTL, 12 | Service: env.DNS_SERVICE || cleanImageName(container.Image), 13 | Instance: env.DNS_INSTANCE || removeSlash(container.Name), 14 | Host: container.NetworkSettings.IpAddress 15 | }; 16 | } 17 | 18 | // docker returns env vars in an array separated by = 19 | // we need to convert this into a key value map 20 | function createEnvironment(container) { 21 | var out = {}; 22 | for (var i = 0; i < container.Config.Env.length; i++) { 23 | var full = container.Config.Env[i]; 24 | var parts = full.split("="); 25 | 26 | if (parts[0].indexOf("DNS_") === 0) { 27 | out[parts[0]] = parts[1]; 28 | } 29 | }; 30 | return out; 31 | } 32 | -------------------------------------------------------------------------------- /plugins/default.js: -------------------------------------------------------------------------------- 1 | function createService(container) { 2 | var port = getDefaultPort(container); 3 | return { 4 | Port: port, 5 | Environment: defaultEnvironment, 6 | TTL: defaultTTL, 7 | Service: cleanImageName(container.Image), 8 | Instance: removeSlash(container.Name), 9 | Host: container.NetworkSettings.IpAddress 10 | }; 11 | } 12 | 13 | function getDefaultPort(container) { 14 | // if we have any exposed ports use those 15 | var port = 0; 16 | var ports = container.NetworkSettings.Ports; 17 | if (Object.keys(ports).length > 0) { 18 | for (var key in ports) { 19 | var value = ports[key]; 20 | if (value !== null && value.length > 0) { 21 | for (var i = 0; i < value.length; i++) { 22 | var hp = parseInt(value[i].HostPort); 23 | if (port === 0 || hp < port) { 24 | port = hp; 25 | } 26 | } 27 | } else if (port === 0) { 28 | // just grab the key value 29 | var expose = parseInt(key.split("/")[0]); 30 | port = expose; 31 | } 32 | } 33 | } 34 | 35 | if (port === 0) { 36 | port = 80; 37 | } 38 | return port; 39 | } 40 | -------------------------------------------------------------------------------- /skydns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/skynetservices/skydns1/msg" 5 | ) 6 | 7 | // Interface to allow mocking of the 8 | // skydns client 9 | type Skydns interface { 10 | Add(uuid string, service *msg.Service) error 11 | Delete(uuid string) error 12 | Update(uuid string, ttl uint32) error 13 | } 14 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func Truncate(name string) string { 8 | if len(name) > 10 { 9 | return name[:10] 10 | } 11 | return name 12 | } 13 | 14 | func checkTag(name string) (bool, int) { 15 | index := strings.LastIndex(name, ":") 16 | if index == -1 || strings.Contains(name[index:], "/") { 17 | return false, -1 18 | } 19 | return true, index 20 | } 21 | 22 | func RemoveTag(name string) string { 23 | if hasTag, index := checkTag(name); hasTag { 24 | return name[:index] 25 | } 26 | return name 27 | } 28 | 29 | func RemoveSlash(name string) string { 30 | return strings.Replace(name, "/", "", -1) 31 | } 32 | 33 | func SplitURI(uri string) (string, string) { 34 | arr := strings.Split(uri, "://") 35 | if len(arr) == 1 { 36 | return "unix", arr[0] 37 | } 38 | prot := arr[0] 39 | if prot == "http" { 40 | prot = "tcp" 41 | } 42 | return prot, arr[1] 43 | } 44 | 45 | func CleanImageName(name string) string { 46 | parts := strings.SplitN(name, "/", 2) 47 | if len(parts) == 1 { 48 | return RemoveSlash(RemoveTag(name)) 49 | } 50 | return CleanImageName(parts[1]) 51 | } 52 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTruncateName(t *testing.T) { 8 | var ( 9 | name = "thisnameis12" 10 | expected = "thisnameis" 11 | ) 12 | 13 | if actual := Truncate(name); actual != expected { 14 | t.Fatalf("Expected %s got %s", expected, actual) 15 | } 16 | } 17 | 18 | func TestRemoveTag(t *testing.T) { 19 | var ( 20 | name = "crosbymichael/redis:latest" 21 | expected = "crosbymichael/redis" 22 | ) 23 | 24 | if actual := RemoveTag(name); actual != expected { 25 | t.Fatalf("Expected %s go %s", expected, actual) 26 | } 27 | } 28 | 29 | func TestRemoveTagWithRegistry(t *testing.T) { 30 | var ( 31 | name = "registry:5000/crosbymichael/redis:latest" 32 | expected = "registry:5000/crosbymichael/redis" 33 | ) 34 | 35 | if actual := RemoveTag(name); actual != expected { 36 | t.Fatalf("Expected %s got %s", expected, actual) 37 | } 38 | } 39 | 40 | func TestRemoveTagWithRegistryNoTag(t *testing.T) { 41 | var ( 42 | name = "registry:5000/crosbymichael/redis" 43 | expected = "registry:5000/crosbymichael/redis" 44 | ) 45 | 46 | if actual := RemoveTag(name); actual != expected { 47 | t.Fatalf("Expected %s got %s", expected, actual) 48 | } 49 | } 50 | 51 | func TestCleanImageName(t *testing.T) { 52 | var ( 53 | name = "crosbymichael/redis:latest" 54 | expected = "redis" 55 | ) 56 | 57 | if actual := CleanImageName(name); actual != expected { 58 | t.Fatalf("Expected %s got %s", expected, actual) 59 | } 60 | } 61 | 62 | func TestCleanImageNameWithRegistry(t *testing.T) { 63 | var ( 64 | name = "registry:5000/crosbymichael/redis:latest" 65 | expected = "redis" 66 | ) 67 | 68 | if actual := CleanImageName(name); actual != expected { 69 | t.Fatalf("Expected %s got %s", expected, actual) 70 | } 71 | } 72 | 73 | func TestCleanImageNameNoParts(t *testing.T) { 74 | var ( 75 | name = "redis:latest" 76 | expected = "redis" 77 | ) 78 | 79 | if actual := CleanImageName(name); actual != expected { 80 | t.Fatalf("Expected %s got %s", expected, actual) 81 | } 82 | } 83 | 84 | func TestSplitURIPathOnly(t *testing.T) { 85 | var ( 86 | uri = "/var/run/docker.sock" 87 | expected_prot = "unix" 88 | expected_path = "/var/run/docker.sock" 89 | ) 90 | 91 | actual_prot, actual_path := SplitURI(uri); 92 | if actual_prot != expected_prot { 93 | t.Fatalf("Expected %s got %s", expected_prot, actual_prot) 94 | } 95 | if actual_path != expected_path { 96 | t.Fatalf("Expected %s got %s", expected_path, actual_path) 97 | } 98 | } 99 | 100 | func TestSplitURIUnix(t *testing.T) { 101 | var ( 102 | uri = "unix:///var/run/docker.sock" 103 | expected_prot = "unix" 104 | expected_path = "/var/run/docker.sock" 105 | ) 106 | 107 | actual_prot, actual_path := SplitURI(uri); 108 | if actual_prot != expected_prot { 109 | t.Fatalf("Expected %s got %s", expected_prot, actual_prot) 110 | } 111 | if actual_path != expected_path { 112 | t.Fatalf("Expected %s got %s", expected_path, actual_path) 113 | } 114 | } 115 | 116 | func TestSplitURITcp(t *testing.T) { 117 | var ( 118 | uri = "tcp://172.17.42.1:4243" 119 | expected_prot = "tcp" 120 | expected_path = "172.17.42.1:4243" 121 | ) 122 | 123 | actual_prot, actual_path := SplitURI(uri); 124 | if actual_prot != expected_prot { 125 | t.Fatalf("Expected %s got %s", expected_prot, actual_prot) 126 | } 127 | if actual_path != expected_path { 128 | t.Fatalf("Expected %s got %s", expected_path, actual_path) 129 | } 130 | } 131 | 132 | func TestSplitURIHttp(t *testing.T) { 133 | var ( 134 | uri = "http://172.17.42.1:4243" 135 | expected_prot = "tcp" 136 | expected_path = "172.17.42.1:4243" 137 | ) 138 | 139 | actual_prot, actual_path := SplitURI(uri); 140 | if actual_prot != expected_prot { 141 | t.Fatalf("Expected %s got %s", expected_prot, actual_prot) 142 | } 143 | if actual_path != expected_path { 144 | t.Fatalf("Expected %s got %s", expected_path, actual_path) 145 | } 146 | } --------------------------------------------------------------------------------