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  }