|
| 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 | +} |
0 commit comments