github.com/chasestarr/deis@v1.13.5-0.20170519182049-1d9e59fbdbfc/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 var wg sync.WaitGroup 79 for _, container := range containers { 80 wg.Add(1) 81 go func(container docker.APIContainers, ttl time.Duration) { 82 defer wg.Done() 83 // send container to channel for processing 84 s.publishContainer(&container, ttl) 85 }(container, ttl) 86 } 87 // Wait for all publish operations to complete. 88 wg.Wait() 89 } 90 91 // getContainer retrieves a container from the docker client based on id 92 func (s *Server) getContainer(id string) (*docker.APIContainers, error) { 93 containers, err := s.DockerClient.ListContainers(docker.ListContainersOptions{}) 94 if err != nil { 95 return nil, err 96 } 97 for _, container := range containers { 98 // send container to channel for processing 99 if container.ID == id { 100 return &container, nil 101 } 102 } 103 return nil, fmt.Errorf("could not find container with id %v", id) 104 } 105 106 // publishContainer publishes the docker container to etcd. 107 func (s *Server) publishContainer(container *docker.APIContainers, ttl time.Duration) { 108 r := regexp.MustCompile(appNameRegex) 109 for _, name := range container.Names { 110 // HACK: remove slash from container name 111 // see https://github.com/docker/docker/issues/7519 112 containerName := name[1:] 113 match := r.FindStringSubmatch(containerName) 114 if match == nil { 115 continue 116 } 117 appName := match[1] 118 appPath := fmt.Sprintf("%s/%s", appName, containerName) 119 keyPath := fmt.Sprintf("/deis/services/%s", appPath) 120 for _, p := range container.Ports { 121 var delay int 122 var timeout int 123 var err error 124 // lowest port wins (docker sorts the ports) 125 // TODO (bacongobbler): support multiple exposed ports 126 port := strconv.Itoa(int(p.PublicPort)) 127 hostAndPort := s.host + ":" + port 128 if s.IsPublishableApp(containerName) && s.IsPortOpen(hostAndPort) { 129 configKey := fmt.Sprintf("/deis/config/%s/", appName) 130 // check if the user specified a healthcheck URL 131 healthcheckURL := s.getEtcd(configKey + "healthcheck_url") 132 initialDelay := s.getEtcd(configKey + "healthcheck_initial_delay") 133 if initialDelay != "" { 134 delay, err = strconv.Atoi(initialDelay) 135 if err != nil { 136 log.Println(err) 137 delay = 0 138 } 139 } else { 140 delay = 0 141 } 142 healthcheckTimeout := s.getEtcd(configKey + "healthcheck_timeout") 143 if healthcheckTimeout != "" { 144 timeout, err = strconv.Atoi(healthcheckTimeout) 145 if err != nil { 146 log.Println(err) 147 timeout = 1 148 } 149 } else { 150 timeout = 1 151 } 152 if healthcheckURL != "" { 153 if !s.HealthCheckOK("http://"+hostAndPort+healthcheckURL, delay, timeout) { 154 continue 155 } 156 } 157 s.setEtcd(keyPath, hostAndPort, uint64(ttl.Seconds())) 158 safeMap.Lock() 159 safeMap.data[container.ID] = appPath 160 safeMap.Unlock() 161 } 162 break 163 } 164 } 165 } 166 167 // removeContainer remove a container published by this component 168 func (s *Server) removeContainer(event string) { 169 safeMap.RLock() 170 appPath := safeMap.data[event] 171 safeMap.RUnlock() 172 173 if appPath != "" { 174 keyPath := fmt.Sprintf("/deis/services/%s", appPath) 175 log.Printf("stopped %s\n", keyPath) 176 s.removeEtcd(keyPath, false) 177 } 178 } 179 180 // IsPublishableApp determines if the application should be published to etcd. 181 func (s *Server) IsPublishableApp(name string) bool { 182 r := regexp.MustCompile(appNameRegex) 183 match := r.FindStringSubmatch(name) 184 if match == nil { 185 return false 186 } 187 appName := match[1] 188 version, err := strconv.Atoi(match[2]) 189 if err != nil { 190 log.Println(err) 191 return false 192 } 193 194 if version >= latestRunningVersion(s.EtcdClient, appName) { 195 return true 196 } 197 return false 198 } 199 200 // IsPortOpen checks if the given port is accepting tcp connections 201 func (s *Server) IsPortOpen(hostAndPort string) bool { 202 portOpen := false 203 conn, err := net.Dial("tcp", hostAndPort) 204 if err == nil { 205 portOpen = true 206 defer conn.Close() 207 } 208 return portOpen 209 } 210 211 func (s *Server) HealthCheckOK(url string, delay, timeout int) bool { 212 // sleep for the initial delay 213 time.Sleep(time.Duration(delay) * time.Second) 214 client := http.Client{ 215 Timeout: time.Duration(timeout) * time.Second, 216 } 217 resp, err := client.Get(url) 218 if err != nil { 219 log.Printf("an error occurred while performing a health check at %s (%v)\n", url, err) 220 return false 221 } 222 if resp.StatusCode != http.StatusOK { 223 log.Printf("healthcheck failed for %s (expected %d, got %d)\n", url, http.StatusOK, resp.StatusCode) 224 } 225 return resp.StatusCode == http.StatusOK 226 } 227 228 // latestRunningVersion retrieves the highest version of the application published 229 // to etcd. If no app has been published, returns 0. 230 func latestRunningVersion(client *etcd.Client, appName string) int { 231 r := regexp.MustCompile(appNameRegex) 232 if client == nil { 233 // FIXME: client should only be nil during tests. This should be properly refactored. 234 if appName == "ceci-nest-pas-une-app" { 235 return 3 236 } 237 return 0 238 } 239 resp, err := client.Get(fmt.Sprintf("/deis/services/%s", appName), false, true) 240 if err != nil { 241 // no app has been published here (key not found) or there was an error 242 return 0 243 } 244 var versions []int 245 for _, node := range resp.Node.Nodes { 246 match := r.FindStringSubmatch(node.Key) 247 // account for keys that may not be an application container 248 if match == nil { 249 continue 250 } 251 version, err := strconv.Atoi(match[2]) 252 if err != nil { 253 log.Println(err) 254 return 0 255 } 256 versions = append(versions, version) 257 } 258 return max(versions) 259 } 260 261 // max returns the maximum value in n 262 func max(n []int) int { 263 val := 0 264 for _, i := range n { 265 if i > val { 266 val = i 267 } 268 } 269 return val 270 } 271 272 // getEtcd retrieves the etcd key's value. Returns an empty string if the key was not found. 273 func (s *Server) getEtcd(key string) string { 274 if s.logLevel == "debug" { 275 log.Println("get", key) 276 } 277 resp, err := s.EtcdClient.Get(key, false, false) 278 if err != nil { 279 return "" 280 } 281 if resp != nil && resp.Node != nil { 282 return resp.Node.Value 283 } 284 return "" 285 } 286 287 // setEtcd sets the corresponding etcd key with the value and ttl 288 func (s *Server) setEtcd(key, value string, ttl uint64) { 289 if _, err := s.EtcdClient.Set(key, value, ttl); err != nil { 290 log.Println(err) 291 } 292 if s.logLevel == "debug" { 293 log.Println("set", key, "->", value) 294 } 295 } 296 297 // removeEtcd removes the corresponding etcd key 298 func (s *Server) removeEtcd(key string, recursive bool) { 299 if _, err := s.EtcdClient.Delete(key, recursive); err != nil { 300 log.Println(err) 301 } 302 if s.logLevel == "debug" { 303 log.Println("del", key) 304 } 305 }