github.com/inflatablewoman/deis@v1.0.1-0.20141111034523-a4511c46a6ce/publisher/server/publisher.go (about) 1 package server 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "os" 8 "regexp" 9 "strconv" 10 "time" 11 12 "github.com/coreos/go-etcd/etcd" 13 "github.com/fsouza/go-dockerclient" 14 ) 15 16 const ( 17 appNameRegex string = `([a-z0-9-]+)_v([1-9][0-9]*).(cmd|web).([1-9][0-9])*` 18 ) 19 20 // Server is the main entrypoint for a publisher. It listens on a docker client for events 21 // and publishes their host:port to the etcd client. 22 type Server struct { 23 DockerClient *docker.Client 24 EtcdClient *etcd.Client 25 } 26 27 // Listen adds an event listener to the docker client and publishes containers that were started. 28 func (s *Server) Listen(ttl time.Duration) { 29 listener := make(chan *docker.APIEvents) 30 // TODO: figure out why we need to sleep for 10 milliseconds 31 // https://github.com/fsouza/go-dockerclient/blob/0236a64c6c4bd563ec277ba00e370cc753e1677c/event_test.go#L43 32 defer func() { time.Sleep(10 * time.Millisecond); s.DockerClient.RemoveEventListener(listener) }() 33 if err := s.DockerClient.AddEventListener(listener); err != nil { 34 log.Fatal(err) 35 } 36 for { 37 select { 38 case event := <-listener: 39 if event.Status == "start" { 40 container, err := s.getContainer(event.ID) 41 if err != nil { 42 log.Println(err) 43 continue 44 } 45 s.publishContainer(container, ttl) 46 } 47 } 48 } 49 } 50 51 // Poll lists all containers from the docker client every time the TTL comes up and publishes them to etcd 52 func (s *Server) Poll(ttl time.Duration) { 53 containers, err := s.DockerClient.ListContainers(docker.ListContainersOptions{}) 54 if err != nil { 55 log.Fatal(err) 56 } 57 for _, container := range containers { 58 // send container to channel for processing 59 s.publishContainer(&container, ttl) 60 } 61 } 62 63 // getContainer retrieves a container from the docker client based on id 64 func (s *Server) getContainer(id string) (*docker.APIContainers, error) { 65 containers, err := s.DockerClient.ListContainers(docker.ListContainersOptions{}) 66 if err != nil { 67 return nil, err 68 } 69 for _, container := range containers { 70 // send container to channel for processing 71 if container.ID == id { 72 return &container, nil 73 } 74 } 75 return nil, errors.New("could not find container") 76 } 77 78 // publishContainer publishes the docker container to etcd. 79 func (s *Server) publishContainer(container *docker.APIContainers, ttl time.Duration) { 80 r := regexp.MustCompile(appNameRegex) 81 host := os.Getenv("HOST") 82 for _, name := range container.Names { 83 // HACK: remove slash from container name 84 // see https://github.com/docker/docker/issues/7519 85 containerName := name[1:] 86 match := r.FindStringSubmatch(containerName) 87 if match == nil { 88 continue 89 } 90 appName := match[1] 91 keyPath := fmt.Sprintf("/deis/services/%s/%s", appName, containerName) 92 for _, p := range container.Ports { 93 port := strconv.Itoa(int(p.PublicPort)) 94 if s.IsPublishableApp(containerName) { 95 s.setEtcd(keyPath, host+":"+port, uint64(ttl.Seconds())) 96 } 97 // TODO: support multiple exposed ports 98 break 99 } 100 } 101 } 102 103 // isPublishableApp determines if the application should be published to etcd. 104 func (s *Server) IsPublishableApp(name string) bool { 105 r := regexp.MustCompile(appNameRegex) 106 match := r.FindStringSubmatch(name) 107 if match == nil { 108 return false 109 } 110 appName := match[1] 111 version, err := strconv.Atoi(match[2]) 112 if err != nil { 113 log.Println(err) 114 return false 115 } 116 if version >= latestRunningVersion(s.EtcdClient, appName) { 117 return true 118 } else { 119 return false 120 } 121 } 122 123 // latestRunningVersion retrieves the highest version of the application published 124 // to etcd. If no app has been published, returns 0. 125 func latestRunningVersion(client *etcd.Client, appName string) int { 126 r := regexp.MustCompile(appNameRegex) 127 if client == nil { 128 // FIXME: client should only be nil during tests. This should be properly refactored. 129 if appName == "ceci-nest-pas-une-app" { 130 return 3 131 } 132 return 0 133 } 134 resp, err := client.Get(fmt.Sprintf("/deis/services/%s", appName), false, true) 135 if err != nil { 136 // no app has been published here (key not found) or there was an error 137 return 0 138 } 139 var versions []int 140 for _, node := range resp.Node.Nodes { 141 match := r.FindStringSubmatch(node.Key) 142 // account for keys that may not be an application container 143 if match == nil { 144 continue 145 } 146 version, err := strconv.Atoi(match[2]) 147 if err != nil { 148 log.Println(err) 149 return 0 150 } 151 versions = append(versions, version) 152 } 153 return max(versions) 154 } 155 156 // max returns the maximum value in n 157 func max(n []int) int { 158 val := 0 159 for _, i := range n { 160 if i > val { 161 val = i 162 } 163 } 164 return val 165 } 166 167 // setEtcd sets the corresponding etcd key with the value and ttl 168 func (s *Server) setEtcd(key, value string, ttl uint64) { 169 if _, err := s.EtcdClient.Set(key, value, ttl); err != nil { 170 log.Println(err) 171 } 172 log.Println("set", key, "->", value) 173 }