1package testflight_test 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "os" 10 "os/exec" 11 "regexp" 12 "runtime" 13 "strconv" 14 "strings" 15 "testing" 16 "time" 17 18 . "github.com/onsi/ginkgo" 19 . "github.com/onsi/gomega" 20 21 "github.com/concourse/concourse/go-concourse/concourse" 22 uuid "github.com/nu7hatch/gouuid" 23 "github.com/onsi/gomega/gexec" 24) 25 26const testflightFlyTarget = "tf" 27const adminFlyTarget = "tf-admin" 28 29const pipelinePrefix = "tf-pipeline" 30const teamName = "testflight" 31 32var flyTarget string 33 34type suiteConfig struct { 35 FlyBin string `json:"fly"` 36 ATCURL string `json:"atc_url"` 37 ATCUsername string `json:"atc_username"` 38 ATCPassword string `json:"atc_password"` 39 DownloadCLI bool `json:"download_cli"` 40} 41 42var ( 43 config = suiteConfig{ 44 ATCURL: "http://localhost:8080", 45 ATCUsername: "test", 46 ATCPassword: "test", 47 } 48 49 pipelineName string 50 tmp string 51) 52 53func TestTestflight(t *testing.T) { 54 RegisterFailHandler(Fail) 55 RunSpecs(t, "TestFlight Suite") 56} 57 58var _ = SynchronizedBeforeSuite(func() []byte { 59 atcURL := os.Getenv("ATC_URL") 60 if atcURL != "" { 61 config.ATCURL = atcURL 62 } 63 64 var err error 65 downloadCLI := os.Getenv("DOWNLOAD_CLI") 66 if downloadCLI != "" { 67 config.DownloadCLI, err = strconv.ParseBool(downloadCLI) 68 Expect(err).ToNot(HaveOccurred()) 69 } 70 71 if config.DownloadCLI { 72 config.FlyBin, err = downloadFly(config.ATCURL) 73 Expect(err).ToNot(HaveOccurred()) 74 } else { 75 config.FlyBin, err = gexec.Build("github.com/concourse/concourse/fly") 76 Expect(err).ToNot(HaveOccurred()) 77 } 78 79 atcUsername := os.Getenv("ATC_USERNAME") 80 if atcUsername != "" { 81 config.ATCUsername = atcUsername 82 } 83 84 atcPassword := os.Getenv("ATC_PASSWORD") 85 if atcPassword != "" { 86 config.ATCPassword = atcPassword 87 } 88 89 payload, err := json.Marshal(config) 90 Expect(err).ToNot(HaveOccurred()) 91 92 Eventually(func() *gexec.Session { 93 login := spawnFlyLogin(adminFlyTarget) 94 <-login.Exited 95 return login 96 }, 2*time.Minute, time.Second).Should(gexec.Exit(0)) 97 98 fly("-t", adminFlyTarget, "set-team", "--non-interactive", "-n", teamName, "--local-user", config.ATCUsername) 99 wait(spawnFlyLogin(testflightFlyTarget, "-n", teamName), false) 100 101 for _, ps := range flyTable("-t", adminFlyTarget, "pipelines") { 102 name := ps["name"] 103 if strings.HasPrefix(name, pipelinePrefix) { 104 fly("-t", adminFlyTarget, "destroy-pipeline", "-n", "-p", name) 105 } 106 } 107 108 for _, ps := range flyTable("-t", testflightFlyTarget, "pipelines") { 109 name := ps["name"] 110 if strings.HasPrefix(name, pipelinePrefix) { 111 fly("-t", testflightFlyTarget, "destroy-pipeline", "-n", "-p", name) 112 } 113 } 114 115 return payload 116}, func(data []byte) { 117 err := json.Unmarshal(data, &config) 118 Expect(err).ToNot(HaveOccurred()) 119}) 120 121var _ = SynchronizedAfterSuite(func() { 122}, func() { 123 os.Remove(config.FlyBin) 124}) 125 126var _ = BeforeEach(func() { 127 SetDefaultEventuallyTimeout(5 * time.Minute) 128 SetDefaultEventuallyPollingInterval(time.Second) 129 SetDefaultConsistentlyDuration(time.Minute) 130 SetDefaultConsistentlyPollingInterval(time.Second) 131 132 var err error 133 tmp, err = ioutil.TempDir("", "testflight-tmp") 134 Expect(err).ToNot(HaveOccurred()) 135 136 flyTarget = testflightFlyTarget 137 138 pipelineName = randomPipelineName() 139}) 140 141var _ = AfterEach(func() { 142 Expect(os.RemoveAll(tmp)).To(Succeed()) 143 144 fly("destroy-pipeline", "-n", "-p", pipelineName) 145}) 146 147func downloadFly(atcUrl string) (string, error) { 148 client := concourse.NewClient(atcUrl, http.DefaultClient, false) 149 readCloser, _, err := client.GetCLIReader("amd64", runtime.GOOS) 150 if err != nil { 151 return "", err 152 } 153 outFile, err := ioutil.TempFile("", "fly") 154 if err != nil { 155 return "", err 156 } 157 defer outFile.Close() 158 _, err = io.Copy(outFile, readCloser) 159 if err != nil { 160 return "", err 161 } 162 err = outFile.Chmod(0755) 163 if err != nil { 164 return "", err 165 } 166 return outFile.Name(), nil 167} 168 169func randomPipelineName() string { 170 guid, err := uuid.NewV4() 171 Expect(err).ToNot(HaveOccurred()) 172 173 return fmt.Sprintf("%s-%d-%s", pipelinePrefix, GinkgoParallelNode(), guid) 174} 175 176func fly(argv ...string) *gexec.Session { 177 sess := spawnFly(argv...) 178 wait(sess, false) 179 return sess 180} 181 182func flyIn(dir string, argv ...string) *gexec.Session { 183 sess := spawnFlyIn(dir, argv...) 184 wait(sess, false) 185 return sess 186} 187 188func flyUnsafe(argv ...string) *gexec.Session { 189 sess := spawnFly(argv...) 190 wait(sess, true) 191 return sess 192} 193 194func spawnFlyLogin(target string, args ...string) *gexec.Session { 195 return spawn(config.FlyBin, append([]string{"-t", target, "login", "-c", config.ATCURL, "-u", config.ATCUsername, "-p", config.ATCPassword}, args...)...) 196} 197 198func spawnFly(argv ...string) *gexec.Session { 199 return spawn(config.FlyBin, append([]string{"-t", flyTarget}, argv...)...) 200} 201 202func spawnFlyIn(dir string, argv ...string) *gexec.Session { 203 return spawnIn(dir, config.FlyBin, append([]string{"-t", flyTarget}, argv...)...) 204} 205 206func spawn(argc string, argv ...string) *gexec.Session { 207 By("running: " + argc + " " + strings.Join(argv, " ")) 208 cmd := exec.Command(argc, argv...) 209 session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 210 Expect(err).ToNot(HaveOccurred()) 211 return session 212} 213 214func spawnIn(dir string, argc string, argv ...string) *gexec.Session { 215 By("running in " + dir + ": " + argc + " " + strings.Join(argv, " ")) 216 cmd := exec.Command(argc, argv...) 217 cmd.Dir = dir 218 session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 219 Expect(err).ToNot(HaveOccurred()) 220 return session 221} 222 223func wait(session *gexec.Session, allowNonZero bool) { 224 <-session.Exited 225 if !allowNonZero { 226 Expect(session.ExitCode()).To(Equal(0), "Output: "+string(session.Out.Contents())) 227 } 228} 229 230var colSplit = regexp.MustCompile(`\s{2,}`) 231 232func flyTable(argv ...string) []map[string]string { 233 session := spawnFly(append([]string{"--print-table-headers"}, argv...)...) 234 <-session.Exited 235 Expect(session.ExitCode()).To(Equal(0)) 236 237 result := []map[string]string{} 238 var headers []string 239 240 rows := strings.Split(string(session.Out.Contents()), "\n") 241 for i, row := range rows { 242 columns := colSplit.Split(strings.TrimSpace(row), -1) 243 244 if i == 0 { 245 headers = columns 246 continue 247 } 248 249 if row == "" { 250 continue 251 } 252 253 result = append(result, map[string]string{}) 254 255 Expect(columns).To(HaveLen(len(headers))) 256 257 for j, header := range headers { 258 if header == "" || columns[j] == "" { 259 continue 260 } 261 262 result[i-1][header] = columns[j] 263 } 264 } 265 266 return result 267} 268 269func setAndUnpausePipeline(config string, args ...string) { 270 setPipeline(config, args...) 271 fly("unpause-pipeline", "-p", pipelineName) 272} 273 274func setPipeline(config string, args ...string) { 275 sp := []string{"set-pipeline", "-n", "-p", pipelineName, "-c", config} 276 fly(append(sp, args...)...) 277} 278 279func inPipeline(thing string) string { 280 return pipelineName + "/" + thing 281} 282 283func newMockVersion(resourceName string, tag string) string { 284 guid, err := uuid.NewV4() 285 Expect(err).ToNot(HaveOccurred()) 286 287 version := guid.String() + "-" + tag 288 289 fly("check-resource", "-r", inPipeline(resourceName), "-f", "version:"+version) 290 291 return version 292} 293 294func waitForBuildAndWatch(jobName string, buildName ...string) *gexec.Session { 295 args := []string{"watch", "-j", inPipeline(jobName)} 296 297 if len(buildName) > 0 { 298 args = append(args, "-b", buildName[0]) 299 } 300 301 keepPollingCheck := regexp.MustCompile("job has no builds|build not found|failed to get build") 302 for { 303 session := spawnFly(args...) 304 <-session.Exited 305 306 if session.ExitCode() == 1 { 307 output := strings.TrimSpace(string(session.Err.Contents())) 308 if keepPollingCheck.MatchString(output) { 309 // build hasn't started yet; keep polling 310 time.Sleep(time.Second) 311 continue 312 } 313 } 314 315 return session 316 } 317} 318 319func withFlyTarget(target string, f func()) { 320 before := flyTarget 321 flyTarget = target 322 f() 323 flyTarget = before 324} 325