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 }