github.com/cloudfoundry-attic/ltc@v0.0.0-20151123212628-098adc7919fc/cluster_test/cluster_test_runner.go (about)

     1  package cluster_test
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"math/rand"
    10  	"net"
    11  	"net/http"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"runtime"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	uuid "github.com/nu7hatch/gouuid"
    21  	. "github.com/onsi/ginkgo"
    22  	ginkgo_config "github.com/onsi/ginkgo/config"
    23  	. "github.com/onsi/gomega"
    24  	"github.com/onsi/gomega/gbytes"
    25  	"github.com/onsi/gomega/gexec"
    26  
    27  	"github.com/cloudfoundry-incubator/ltc/config"
    28  	"github.com/cloudfoundry-incubator/ltc/terminal/colors"
    29  	"github.com/cloudfoundry-incubator/ltc/test_helpers"
    30  )
    31  
    32  var numCPU int
    33  
    34  func init() {
    35  	numCPU = runtime.NumCPU()
    36  	runtime.GOMAXPROCS(numCPU)
    37  }
    38  
    39  type ClusterTestRunner interface {
    40  	Run(timeout time.Duration, verbose bool)
    41  }
    42  
    43  type clusterTestRunner struct {
    44  	testingT          GinkgoTestingT
    45  	config            *config.Config
    46  	latticeCliHome    string
    47  	ltcExecutablePath string
    48  }
    49  
    50  type ginkgoTestingT struct{}
    51  
    52  func (g *ginkgoTestingT) Fail() {
    53  	fmt.Println("")
    54  	os.Exit(1)
    55  }
    56  
    57  func forceAbs(path string) string {
    58  	if filepath.IsAbs(path) || !strings.Contains(path, "/") {
    59  		return path
    60  	}
    61  
    62  	abs, err := filepath.Abs(os.Args[0])
    63  	if err != nil {
    64  		panic(err)
    65  	}
    66  	return abs
    67  }
    68  
    69  func NewClusterTestRunner(config *config.Config, latticeCliHome string) ClusterTestRunner {
    70  	return &clusterTestRunner{
    71  		config:            config,
    72  		testingT:          &ginkgoTestingT{},
    73  		latticeCliHome:    latticeCliHome,
    74  		ltcExecutablePath: forceAbs(os.Args[0]),
    75  	}
    76  }
    77  
    78  func (runner *clusterTestRunner) Run(timeout time.Duration, verbose bool) {
    79  	ginkgo_config.DefaultReporterConfig.Verbose = verbose
    80  	ginkgo_config.DefaultReporterConfig.SlowSpecThreshold = float64(45)
    81  	ginkgo_config.DefaultReporterConfig.NoColor = os.Getenv("TERM") == ""
    82  	defineTheGinkgoTests(runner, timeout)
    83  	RegisterFailHandler(Fail)
    84  	RunSpecs(runner.testingT, "Lattice Integration Tests")
    85  	fmt.Println("")
    86  }
    87  
    88  func defineTheGinkgoTests(runner *clusterTestRunner, timeout time.Duration) {
    89  	BeforeSuite(func() {
    90  		if err := runner.config.Load(); err != nil {
    91  			fmt.Fprintln(getStyledWriter("test"), "Error loading config")
    92  			return
    93  		}
    94  	})
    95  
    96  	AfterSuite(func() {
    97  		gexec.CleanupBuildArtifacts()
    98  	})
    99  
   100  	Describe("Lattice cluster", func() {
   101  		Describe("docker apps with HTTP routes", func() {
   102  			var appName, appRoute string
   103  
   104  			BeforeEach(func() {
   105  				appGUID, err := uuid.NewV4()
   106  				Expect(err).NotTo(HaveOccurred())
   107  
   108  				appName = fmt.Sprintf("lattice-test-app-%s", appGUID.String())
   109  				appRoute = fmt.Sprintf("%s.%s", appName, runner.config.Target())
   110  			})
   111  
   112  			AfterEach(func() {
   113  				runner.removeApp(timeout, appName, fmt.Sprintf("--timeout=%s", timeout.String()))
   114  
   115  				Eventually(errorCheckForRoute(appRoute), timeout, 1).Should(HaveOccurred())
   116  			})
   117  
   118  			It("should run with the provided ltc options", func() {
   119  				debugLogsStream := runner.streamDebugLogs(timeout)
   120  				defer func() { killSession(debugLogsStream) }()
   121  
   122  				runner.createDockerApp(timeout, appName, "cloudfoundry/lattice-app", fmt.Sprintf("--timeout=%s", timeout.String()))
   123  
   124  				Eventually(errorCheckForRoute(appRoute), timeout, 1).ShouldNot(HaveOccurred())
   125  
   126  				Eventually(debugLogsStream.Out, timeout).Should(gbytes.Say("rep.*lattice-(colocated|cell|brain)-\\d+"))
   127  				Eventually(debugLogsStream.Out, timeout).Should(gbytes.Say("garden-linux.*lattice-(colocated|cell|brain)-\\d+"))
   128  				killSession(debugLogsStream)
   129  
   130  				logsStream := runner.streamLogs(timeout, appName)
   131  				defer func() { killSession(logsStream) }()
   132  
   133  				Eventually(logsStream.Out, timeout).Should(gbytes.Say("Lattice-app. Says Hello."))
   134  
   135  				resp, err := makeGetRequestToURL(appRoute + "/env")
   136  				Expect(err).NotTo(HaveOccurred())
   137  				defer resp.Body.Close()
   138  				respBytes, err := ioutil.ReadAll(resp.Body)
   139  				Expect(err).NotTo(HaveOccurred())
   140  				Expect(respBytes).To(MatchRegexp("<dt>USER</dt><dd>lattice</dd>"))
   141  
   142  				runner.scaleApp(timeout, appName, fmt.Sprintf("--timeout=%s", timeout.String()))
   143  
   144  				instanceCountChan := make(chan int, numCPU)
   145  				go countInstances(appRoute, instanceCountChan)
   146  				Eventually(instanceCountChan, timeout).Should(Receive(Equal(3)))
   147  			})
   148  		})
   149  
   150  		Context("docker apps with TCP routes", func() {
   151  			var appName string
   152  
   153  			BeforeEach(func() {
   154  				appGUID, err := uuid.NewV4()
   155  				Expect(err).NotTo(HaveOccurred())
   156  
   157  				appName = fmt.Sprintf("lattice-test-app-%s", appGUID.String())
   158  			})
   159  
   160  			AfterEach(func() {
   161  				runner.removeApp(timeout, appName, fmt.Sprintf("--timeout=%s", timeout.String()))
   162  			})
   163  
   164  			It("should run with the provided ltc options", func() {
   165  				externalPort := uint16(rand.Intn(9999) + 50000)
   166  				runner.createDockerApp(timeout, appName, "cloudfoundry/lattice-tcp-test", fmt.Sprintf("--tcp-route=%d:5222", externalPort), fmt.Sprintf("--timeout=%s", timeout.String()))
   167  				Eventually(readLineFromConnection(runner.config.Target(), externalPort), timeout, 1).Should(Equal("y"))
   168  
   169  				externalPort++
   170  				By("Updating the routes")
   171  				runner.updateApp(timeout, appName, fmt.Sprintf("--tcp-route=%d:5222", externalPort))
   172  				Eventually(readLineFromConnection(runner.config.Target(), externalPort), timeout, 1).Should(Equal("y"))
   173  			})
   174  		})
   175  
   176  		Context("droplet apps", func() {
   177  			var dropletName, appName, dropletFolderURL, appRoute string
   178  
   179  			BeforeEach(func() {
   180  				dropletGUID, err := uuid.NewV4()
   181  				Expect(err).NotTo(HaveOccurred())
   182  				dropletName = "droplet-" + dropletGUID.String()
   183  
   184  				appName = "running-" + dropletName
   185  
   186  				blobTarget := runner.config.BlobStore()
   187  
   188  				if blobTarget.Username != "" {
   189  					dropletFolderURL = fmt.Sprintf("%s:%s@%s:%s/blobs/%s",
   190  						blobTarget.Username,
   191  						blobTarget.Password,
   192  						blobTarget.Host,
   193  						blobTarget.Port,
   194  						dropletName)
   195  				} else {
   196  					dropletFolderURL = fmt.Sprintf("%s:%s/blobs/%s",
   197  						blobTarget.Host,
   198  						blobTarget.Port,
   199  						dropletName)
   200  				}
   201  
   202  				appRoute = fmt.Sprintf("%s.%s", appName, runner.config.Target())
   203  			})
   204  
   205  			AfterEach(func() {
   206  				runner.removeApp(timeout, appName, fmt.Sprintf("--timeout=%s", timeout.String()))
   207  				Eventually(errorCheckForRoute(appRoute), timeout, .5).Should(HaveOccurred())
   208  
   209  				runner.removeDroplet(timeout, dropletName)
   210  			})
   211  
   212  			It("builds, lists and launches a droplet", func() {
   213  				By("checking out lattice-app from github")
   214  				gitDir := runner.cloneRepo(timeout, "https://github.com/cloudfoundry-samples/lattice-app.git")
   215  				defer os.RemoveAll(gitDir)
   216  
   217  				By("launching a build task")
   218  				runner.buildDroplet(timeout, dropletName, "https://github.com/cloudfoundry/go-buildpack.git", gitDir)
   219  
   220  				Eventually(runner.checkIfTaskCompleted("build-droplet-"+dropletName), timeout, 1).Should(BeTrue())
   221  
   222  				By("listing droplets")
   223  				runner.listDroplets(timeout, dropletName)
   224  
   225  				By("launching the droplet")
   226  				runner.launchDroplet(timeout, appName, dropletName)
   227  
   228  				Eventually(errorCheckForRoute(appRoute), timeout, .5).ShouldNot(HaveOccurred())
   229  			})
   230  		})
   231  	})
   232  }
   233  
   234  func killSession(session *gexec.Session) {
   235  	if runtime.GOOS == "windows" {
   236  		session.Kill().Wait()
   237  	} else {
   238  		session.Terminate().Wait()
   239  	}
   240  }
   241  
   242  func (runner *clusterTestRunner) cloneRepo(timeout time.Duration, repoURL string) string {
   243  	tmpDir, err := ioutil.TempDir("", "repo")
   244  	Expect(err).NotTo(HaveOccurred())
   245  
   246  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Attempting to clone %s to %s", repoURL, tmpDir)))
   247  
   248  	command := exec.Command("git", "clone", repoURL, tmpDir)
   249  	session, err := gexec.Start(command, getStyledWriter("git-clone"), getStyledWriter("git-clone"))
   250  	Expect(err).NotTo(HaveOccurred())
   251  
   252  	expectExitInBuffer(timeout, session, session.Err)
   253  	Eventually(session.Err).Should(test_helpers.Say(fmt.Sprintf("Cloning into '%s'...", tmpDir)))
   254  
   255  	fmt.Fprintf(getStyledWriter("test"), "Cloned %s into %s\n", repoURL, tmpDir)
   256  
   257  	return tmpDir
   258  }
   259  
   260  func (runner *clusterTestRunner) buildDroplet(timeout time.Duration, dropletName, buildpack, srcDir string) {
   261  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Submitting build of %s with buildpack %s", dropletName, buildpack)))
   262  
   263  	command := runner.command("build-droplet", dropletName, buildpack, "--timeout", timeout.String())
   264  	command.Dir = srcDir
   265  	session, err := gexec.Start(command, getStyledWriter("build-droplet"), getStyledWriter("build-droplet"))
   266  	Expect(err).NotTo(HaveOccurred())
   267  
   268  	expectExit(timeout, session)
   269  	Expect(session.Out).To(gbytes.Say("Submitted build of " + dropletName))
   270  	Expect(session.Out).NotTo(gbytes.Say("use of closed network connection"))
   271  }
   272  
   273  func (runner *clusterTestRunner) launchDroplet(timeout time.Duration, appName, dropletName string, args ...string) {
   274  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Launching droplet %s as %s", dropletName, appName)))
   275  
   276  	launchArgs := append([]string{"launch-droplet", appName, dropletName}, args...)
   277  	command := runner.command(launchArgs...)
   278  	session, err := gexec.Start(command, getStyledWriter("launch-droplet"), getStyledWriter("launch-droplet"))
   279  	Expect(err).NotTo(HaveOccurred())
   280  
   281  	expectExit(timeout, session)
   282  	Expect(session.Out).To(gbytes.Say(appName + " is now running."))
   283  }
   284  
   285  func (runner *clusterTestRunner) listDroplets(timeout time.Duration, dropletName string) {
   286  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline("Attempting to find droplet in the list"))
   287  
   288  	command := runner.command("list-droplets")
   289  	session, err := gexec.Start(command, getStyledWriter("list-droplets"), getStyledWriter("list-droplets"))
   290  	Expect(err).NotTo(HaveOccurred())
   291  
   292  	expectExit(timeout, session)
   293  	Expect(session.Out).To(gbytes.Say(dropletName))
   294  
   295  	fmt.Fprintln(getStyledWriter("test"), "Found", dropletName, "in the list!")
   296  }
   297  
   298  func (runner *clusterTestRunner) checkIfTaskCompleted(taskName string) func() bool {
   299  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline("Waiting for task "+taskName+" to complete"))
   300  	return func() bool {
   301  		command := runner.command("task", taskName)
   302  
   303  		session, err := gexec.Start(command, getStyledWriter("task"), getStyledWriter("task"))
   304  		if err != nil {
   305  			panic(err)
   306  		}
   307  		if exitCode := session.Wait().ExitCode(); exitCode != 0 {
   308  			return true
   309  		}
   310  
   311  		return bytes.Contains(session.Out.Contents(), []byte("COMPLETED"))
   312  	}
   313  }
   314  
   315  func (runner *clusterTestRunner) removeDroplet(timeout time.Duration, dropletName string) {
   316  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Attempting to remove droplet %s", dropletName)))
   317  
   318  	command := runner.command("remove-droplet", dropletName)
   319  	session, err := gexec.Start(command, getStyledWriter("remove-droplet"), getStyledWriter("remove-droplet"))
   320  	Expect(err).NotTo(HaveOccurred())
   321  
   322  	expectExit(timeout, session)
   323  	Expect(session.Out).To(gbytes.Say("Droplet removed"))
   324  
   325  	fmt.Fprintln(getStyledWriter("test"), "Removed", dropletName)
   326  }
   327  
   328  func (runner *clusterTestRunner) createDockerApp(timeout time.Duration, appName, dockerPath string, args ...string) {
   329  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Attempting to create %s", appName)))
   330  
   331  	createArgs := append([]string{"create", appName, dockerPath}, args...)
   332  	command := runner.command(createArgs...)
   333  	session, err := gexec.Start(command, getStyledWriter("create"), getStyledWriter("create"))
   334  	Expect(err).NotTo(HaveOccurred())
   335  
   336  	expectExit(timeout, session)
   337  	Expect(session.Out).To(gbytes.Say(appName + " is now running."))
   338  
   339  	fmt.Fprintln(getStyledWriter("test"), "Yay! Created", appName)
   340  }
   341  
   342  func (runner *clusterTestRunner) updateApp(timeout time.Duration, appName string, args ...string) {
   343  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Attempting to update %s", appName)))
   344  	updateArgs := append([]string{"update", appName}, args...)
   345  	command := runner.command(updateArgs...)
   346  
   347  	session, err := gexec.Start(command, getStyledWriter("update"), getStyledWriter("update"))
   348  
   349  	Expect(err).NotTo(HaveOccurred())
   350  	expectExit(timeout, session)
   351  
   352  	Expect(session.Out).To(gbytes.Say("Updating " + appName + " routes"))
   353  	fmt.Fprintln(getStyledWriter("test"), "Yay! updated", appName)
   354  }
   355  
   356  func (runner *clusterTestRunner) streamLogs(timeout time.Duration, appName string, args ...string) *gexec.Session {
   357  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Attempting to stream logs from %s", appName)))
   358  
   359  	command := runner.command("logs", appName)
   360  	session, err := gexec.Start(command, getStyledWriter("logs"), getStyledWriter("logs"))
   361  	Expect(err).NotTo(HaveOccurred())
   362  
   363  	return session
   364  }
   365  
   366  func (runner *clusterTestRunner) streamDebugLogs(timeout time.Duration, args ...string) *gexec.Session {
   367  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline("Attempting to stream cluster debug logs"))
   368  
   369  	command := runner.command("debug-logs")
   370  	session, err := gexec.Start(command, getStyledWriter("debug"), getStyledWriter("debug"))
   371  	Expect(err).NotTo(HaveOccurred())
   372  
   373  	return session
   374  }
   375  
   376  func (runner *clusterTestRunner) scaleApp(timeout time.Duration, appName string, args ...string) {
   377  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Attempting to scale %s", appName)))
   378  
   379  	command := runner.command("scale", appName, "3")
   380  	session, err := gexec.Start(command, getStyledWriter("scale"), getStyledWriter("scale"))
   381  	Expect(err).NotTo(HaveOccurred())
   382  
   383  	expectExit(timeout, session)
   384  	Expect(session.Out).To(gbytes.Say("App Scaled Successfully"))
   385  }
   386  
   387  func (runner *clusterTestRunner) removeApp(timeout time.Duration, appName string, args ...string) {
   388  	fmt.Fprintln(getStyledWriter("test"), colors.PurpleUnderline(fmt.Sprintf("Attempting to remove app %s", appName)))
   389  
   390  	command := runner.command("remove", appName)
   391  	session, err := gexec.Start(command, getStyledWriter("remove"), getStyledWriter("remove"))
   392  	Expect(err).NotTo(HaveOccurred())
   393  
   394  	expectExit(timeout, session)
   395  }
   396  
   397  //TODO: add subcommand string param
   398  func (runner *clusterTestRunner) command(arg ...string) *exec.Cmd {
   399  	command := exec.Command(runner.ltcExecutablePath, arg...)
   400  	cliHome := fmt.Sprintf("LATTICE_CLI_HOME=%s", runner.latticeCliHome)
   401  	command.Env = append(os.Environ(), cliHome)
   402  	return command
   403  }
   404  
   405  func getStyledWriter(prefix string) io.Writer {
   406  	return gexec.NewPrefixedWriter(fmt.Sprintf("[%s] ", colors.Yellow(prefix)), GinkgoWriter)
   407  }
   408  
   409  func readLineFromConnection(ip string, port uint16) func() (string, error) {
   410  	fmt.Fprintln(getStyledWriter("test"), "Connection to ", ip, ":", port)
   411  	return func() (string, error) {
   412  		conn, err := net.Dial("tcp", ip+fmt.Sprintf(":%d", port))
   413  		if err != nil {
   414  			return "", err
   415  		}
   416  		defer conn.Close()
   417  
   418  		conn.SetDeadline(time.Now().Add(time.Second))
   419  
   420  		line, err := bufio.NewReader(conn).ReadString('\n')
   421  		if err != nil {
   422  			return "", err
   423  		}
   424  
   425  		return strings.TrimSpace(line), nil
   426  	}
   427  }
   428  
   429  func errorCheckForRoute(appRoute string) func() error {
   430  	fmt.Fprintln(getStyledWriter("test"), "Polling for the appRoute", appRoute)
   431  	return func() error {
   432  		response, err := makeGetRequestToURL(appRoute)
   433  		if err != nil {
   434  			return err
   435  		}
   436  
   437  		io.Copy(ioutil.Discard, response.Body)
   438  		defer response.Body.Close()
   439  
   440  		if response.StatusCode != http.StatusOK {
   441  			return fmt.Errorf("Status code %d should be 200", response.StatusCode)
   442  		}
   443  
   444  		return nil
   445  	}
   446  }
   447  
   448  func countInstances(appRoute string, instanceCountChan chan<- int) {
   449  	defer GinkgoRecover()
   450  	instanceIndexRoute := fmt.Sprintf("%s/index", appRoute)
   451  	instancesSeen := make(map[int]bool)
   452  
   453  	instanceIndexChan := make(chan int, numCPU)
   454  
   455  	for i := 0; i < numCPU; i++ {
   456  		go pollForInstanceIndices(instanceIndexRoute, instanceIndexChan)
   457  	}
   458  
   459  	for {
   460  		instanceIndex := <-instanceIndexChan
   461  		instancesSeen[instanceIndex] = true
   462  		instanceCountChan <- len(instancesSeen)
   463  	}
   464  }
   465  
   466  func pollForInstanceIndices(appRoute string, instanceIndexChan chan<- int) {
   467  	defer GinkgoRecover()
   468  	for {
   469  		response, err := makeGetRequestToURL(appRoute)
   470  		Expect(err).To(BeNil())
   471  
   472  		responseBody, err := ioutil.ReadAll(response.Body)
   473  		defer response.Body.Close()
   474  		Expect(err).To(BeNil())
   475  
   476  		instanceIndex, err := strconv.Atoi(string(responseBody))
   477  		if err != nil {
   478  			continue
   479  		}
   480  		instanceIndexChan <- instanceIndex
   481  	}
   482  }
   483  
   484  func makeGetRequestToURL(url string) (*http.Response, error) {
   485  	routeWithScheme := fmt.Sprintf("http://%s", url)
   486  	resp, err := http.DefaultClient.Get(routeWithScheme)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	return resp, nil
   492  }
   493  
   494  func expectExit(timeout time.Duration, session *gexec.Session) {
   495  	expectExitInBuffer(timeout, session, session.Out)
   496  }
   497  
   498  func expectExitInBuffer(timeout time.Duration, session *gexec.Session, outputBuffer *gbytes.Buffer) {
   499  	Eventually(session, timeout).Should(gexec.Exit(0))
   500  	Expect(string(outputBuffer.Contents())).To(HaveSuffix("\n"))
   501  }