github.com/greenboxal/deis@v1.12.1/publisher/server/publisher.go (about) 1 package server 2 3 import ( 4 "fmt" 5 "log" 6 "net" 7 "net/http" 8 "regexp" 9 "strconv" 10 "sync" 11 "time" 12 13 "github.com/coreos/go-etcd/etcd" 14 "github.com/fsouza/go-dockerclient" 15 ) 16 17 const ( 18 appNameRegex string = `([a-z0-9-]+)_v([1-9][0-9]*).(cmd|web).([1-9][0-9])*` 19 ) 20 21 // Server is the main entrypoint for a publisher. It listens on a docker client for events 22 // and publishes their host:port to the etcd client. 23 type Server struct { 24 DockerClient *docker.Client 25 EtcdClient *etcd.Client 26 27 host string 28 logLevel string 29 } 30 31 var safeMap = struct { 32 sync.RWMutex 33 data map[string]string 34 }{data: make(map[string]string)} 35 36 // New returns a new instance of Server. 37 func New(dockerClient *docker.Client, etcdClient *etcd.Client, host, logLevel string) *Server { 38 return &Server{ 39 DockerClient: dockerClient, 40 EtcdClient: etcdClient, 41 host: host, 42 logLevel: logLevel, 43 } 44 } 45 46 // Listen adds an event listener to the docker client and publishes containers that were started. 47 func (s *Server) Listen(ttl time.Duration) { 48 listener := make(chan *docker.APIEvents) 49 // TODO: figure out why we need to sleep for 10 milliseconds 50 // https://github.com/fsouza/go-dockerclient/blob/0236a64c6c4bd563ec277ba00e370cc753e1677c/event_test.go#L43 51 defer func() { time.Sleep(10 * time.Millisecond); s.DockerClient.RemoveEventListener(listener) }() 52 if err := s.DockerClient.AddEventListener(listener); err != nil { 53 log.Fatal(err) 54 } 55 for { 56 select { 57 case event := <-listener: 58 if event.Status == "start" { 59 container, err := s.getContainer(event.ID) 60 if err != nil { 61 log.Println(err) 62 continue 63 } 64 s.publishContainer(container, ttl) 65 } else if event.Status == "stop" { 66 s.removeContainer(event.ID) 67 } 68 } 69 } 70 } 71 72 // Poll lists all containers from the docker client every time the TTL comes up and publishes them to etcd 73 func (s *Server) Poll(ttl time.Duration) { 74 containers, err := s.DockerClient.ListContainers(docker.ListContainersOptions{}) 75 if err != nil { 76 log.Fatal(err) 77 } 78 for _, container := range containers { 79 // send container to channel for processing 80 s.publishContainer(&container, ttl) 81 } 82 } 83 84 // getContainer retrieves a container from the docker client based on id 85 func (s *Server) getContainer(id string) (*docker.APIContainers, error) { 86 containers, err := s.DockerClient.ListContainers(docker.ListContainersOptions{}) 87 if err != nil { 88 return nil, err 89 } 90 for _, container := range containers { 91 // send container to channel for processing 92 if container.ID == id { 93 return &container, nil 94 } 95 } 96 return nil, fmt.Errorf("could not find container with id %v", id) 97 } 98 99 // publishContainer publishes the docker container to etcd. 100 func (s *Server) publishContainer(container *docker.APIContainers, ttl time.Duration) { 101 r := regexp.MustCompile(appNameRegex) 102 for _, name := range container.Names { 103 // HACK: remove slash from container name 104 // see https://github.com/docker/docker/issues/7519 105 containerName := name[1:] 106 match := r.FindStringSubmatch(containerName) 107 if match == nil { 108 continue 109 } 110 appName := match[1] 111 appPath := fmt.Sprintf("%s/%s", appName, containerName) 112 keyPath := fmt.Sprintf("/deis/services/%s", appPath) 113 for _, p := range container.Ports { 114 var delay int 115 var timeout int 116 var err error 117 // lowest port wins (docker sorts the ports) 118 // TODO (bacongobbler): support multiple exposed ports 119 port := strconv.Itoa(int(p.PublicPort)) 120 hostAndPort := s.host + ":" + port 121 if s.IsPublishableApp(containerName) && s.IsPortOpen(hostAndPort) { 122 configKey := fmt.Sprintf("/deis/config/%s/", appName) 123 // check if the user specified a healthcheck URL 124 healthcheckURL := s.getEtcd(configKey + "healthcheck_url") 125 initialDelay := s.getEtcd(configKey + "healthcheck_initial_delay") 126 if initialDelay != "" { 127 delay, err = strconv.Atoi(initialDelay) 128 if err != nil { 129 log.Println(err) 130 delay = 0 131 } 132 } else { 133 delay = 0 134 } 135 healthcheckTimeout := s.getEtcd(configKey + "healthcheck_timeout") 136 if healthcheckTimeout != "" { 137 timeout, err = strconv.Atoi(healthcheckTimeout) 138 if err != nil { 139 log.Println(err) 140 timeout = 1 141 } 142 } else { 143 timeout = 1 144 } 145 if healthcheckURL != "" { 146 if !s.HealthCheckOK("http://"+hostAndPort+healthcheckURL, delay, timeout) { 147 continue 148 } 149 } 150 s.setEtcd(keyPath, hostAndPort, uint64(ttl.Seconds())) 151 safeMap.Lock() 152 safeMap.data[container.ID] = appPath 153 safeMap.Unlock() 154 } 155 break 156 } 157 } 158 } 159 160 // removeContainer remove a container published by this component 161 func (s *Server) removeContainer(event string) { 162 safeMap.RLock() 163 appPath := safeMap.data[event] 164 safeMap.RUnlock() 165 166 if appPath != "" { 167 keyPath := fmt.Sprintf("/deis/services/%s", appPath) 168 log.Printf("stopped %s\n", keyPath) 169 s.removeEtcd(keyPath, false) 170 } 171 } 172 173 // IsPublishableApp determines if the application should be published to etcd. 174 func (s *Server) IsPublishableApp(name string) bool { 175 r := regexp.MustCompile(appNameRegex) 176 match := r.FindStringSubmatch(name) 177 if match == nil { 178 return false 179 } 180 appName := match[1] 181 version, err := strconv.Atoi(match[2]) 182 if err != nil { 183 log.Println(err) 184 return false 185 } 186 187 if version >= latestRunningVersion(s.EtcdClient, appName) { 188 return true 189 } 190 return false 191 } 192 193 // IsPortOpen checks if the given port is accepting tcp connections 194 func (s *Server) IsPortOpen(hostAndPort string) bool { 195 portOpen := false 196 conn, err := net.Dial("tcp", hostAndPort) 197 if err == nil { 198 portOpen = true 199 defer conn.Close() 200 } 201 return portOpen 202 } 203 204 func (s *Server) HealthCheckOK(url string, delay, timeout int) bool { 205 // sleep for the initial delay 206 time.Sleep(time.Duration(delay) * time.Second) 207 client := http.Client{ 208 Timeout: time.Duration(timeout) * time.Second, 209 } 210 resp, err := client.Get(url) 211 if err != nil { 212 log.Printf("an error occurred while performing a health check at %s (%v)\n", url, err) 213 return false 214 } 215 if resp.StatusCode != http.StatusOK { 216 log.Printf("healthcheck failed for %s (expected %d, got %d)\n", url, http.StatusOK, resp.StatusCode) 217 } 218 return resp.StatusCode == http.StatusOK 219 } 220 221 // latestRunningVersion retrieves the highest version of the application published 222 // to etcd. If no app has been published, returns 0. 223 func latestRunningVersion(client *etcd.Client, appName string) int { 224 r := regexp.MustCompile(appNameRegex) 225 if client == nil { 226 // FIXME: client should only be nil during tests. This should be properly refactored. 227 if appName == "ceci-nest-pas-une-app" { 228 return 3 229 } 230 return 0 231 } 232 resp, err := client.Get(fmt.Sprintf("/deis/services/%s", appName), false, true) 233 if err != nil { 234 // no app has been published here (key not found) or there was an error 235 return 0 236 } 237 var versions []int 238 for _, node := range resp.Node.Nodes { 239 match := r.FindStringSubmatch(node.Key) 240 // account for keys that may not be an application container 241 if match == nil { 242 continue 243 } 244 version, err := strconv.Atoi(match[2]) 245 if err != nil { 246 log.Println(err) 247 return 0 248 } 249 versions = append(versions, version) 250 } 251 return max(versions) 252 } 253 254 // max returns the maximum value in n 255 func max(n []int) int { 256 val := 0 257 for _, i := range n { 258 if i > val { 259 val = i 260 } 261 } 262 return val 263 } 264 265 // getEtcd retrieves the etcd key's value. Returns an empty string if the key was not found. 266 func (s *Server) getEtcd(key string) string { 267 if s.logLevel == "debug" { 268 log.Println("get", key) 269 } 270 resp, err := s.EtcdClient.Get(key, false, false) 271 if err != nil { 272 return "" 273 } 274 if resp != nil && resp.Node != nil { 275 return resp.Node.Value 276 } 277 return "" 278 } 279 280 // setEtcd sets the corresponding etcd key with the value and ttl 281 func (s *Server) setEtcd(key, value string, ttl uint64) { 282 if _, err := s.EtcdClient.Set(key, value, ttl); err != nil { 283 log.Println(err) 284 } 285 if s.logLevel == "debug" { 286 log.Println("set", key, "->", value) 287 } 288 } 289 290 // removeEtcd removes the corresponding etcd key 291 func (s *Server) removeEtcd(key string, recursive bool) { 292 if _, err := s.EtcdClient.Delete(key, recursive); err != nil { 293 log.Println(err) 294 } 295 if s.logLevel == "debug" { 296 log.Println("del", key) 297 } 298 }