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  }