1package integration_test
2
3import (
4	"archive/tar"
5	"compress/gzip"
6	"encoding/json"
7	"fmt"
8	"io/ioutil"
9	"net/http"
10	"net/url"
11	"os"
12	"os/exec"
13	"path"
14	"path/filepath"
15	"runtime"
16	"strings"
17	"syscall"
18	"time"
19
20	"github.com/concourse/concourse/atc"
21	"github.com/concourse/concourse/atc/event"
22	"github.com/concourse/concourse/atc/testhelpers"
23	. "github.com/onsi/ginkgo"
24	. "github.com/onsi/gomega"
25	"github.com/onsi/gomega/gbytes"
26	"github.com/onsi/gomega/gexec"
27	"github.com/onsi/gomega/ghttp"
28	"github.com/vito/go-sse/sse"
29)
30
31var _ = Describe("Fly CLI", func() {
32	var tmpdir string
33	var buildDir string
34	var taskConfigPath string
35
36	var streaming chan struct{}
37	var events chan atc.Event
38	var uploadedBits chan struct{}
39
40	var expectedPlan atc.Plan
41	var taskPlan atc.Plan
42	var workerArtifact = atc.WorkerArtifact{
43		ID:   125,
44		Name: "some-dir",
45	}
46	var planFactory atc.PlanFactory
47
48	BeforeEach(func() {
49		var err error
50		tmpdir, err = ioutil.TempDir("", "fly-build-dir")
51		Expect(err).NotTo(HaveOccurred())
52
53		buildDir = filepath.Join(tmpdir, "fixture")
54
55		err = os.Mkdir(buildDir, 0755)
56		Expect(err).NotTo(HaveOccurred())
57
58		taskConfigPath = filepath.Join(buildDir, "task.yml")
59
60		err = ioutil.WriteFile(
61			taskConfigPath,
62			[]byte(`---
63platform: some-platform
64
65image_resource:
66  type: registry-image
67  source:
68    repository: ubuntu
69
70inputs:
71- name: fixture
72
73params:
74  FOO: bar
75  BAZ: buzz
76  X: 1
77  EMPTY:
78
79run:
80  path: find
81  args: [.]
82`),
83			0644,
84		)
85		Expect(err).NotTo(HaveOccurred())
86
87		streaming = make(chan struct{})
88		events = make(chan atc.Event)
89
90		planFactory = atc.NewPlanFactory(0)
91
92		taskPlan = planFactory.NewPlan(atc.TaskPlan{
93			Name: "one-off",
94			Config: &atc.TaskConfig{
95				Platform: "some-platform",
96				ImageResource: &atc.ImageResource{
97					Type: "registry-image",
98					Source: atc.Source{
99						"repository": "ubuntu",
100					},
101				},
102				Inputs: []atc.TaskInputConfig{
103					{Name: "fixture"},
104				},
105				Params: map[string]string{
106					"FOO":   "bar",
107					"BAZ":   "buzz",
108					"X":     "1",
109					"EMPTY": "",
110				},
111				Run: atc.TaskRunConfig{
112					Path: "find",
113					Args: []string{"."},
114				},
115			},
116		})
117
118		expectedPlan = planFactory.NewPlan(atc.DoPlan{
119			planFactory.NewPlan(atc.AggregatePlan{
120				planFactory.NewPlan(atc.ArtifactInputPlan{
121					ArtifactID: 125,
122					Name:       filepath.Base(buildDir),
123				}),
124			}),
125			taskPlan,
126		})
127	})
128
129	AfterEach(func() {
130		os.RemoveAll(tmpdir)
131		close(uploadedBits)
132	})
133
134	JustBeforeEach(func() {
135		uploadedBits = make(chan struct{}, 5) // at most there should only be 2 uploads
136		atcServer.RouteToHandler("POST", "/api/v1/teams/main/artifacts",
137			ghttp.CombineHandlers(
138				func(w http.ResponseWriter, req *http.Request) {
139					gr, err := gzip.NewReader(req.Body)
140					Expect(err).NotTo(HaveOccurred())
141
142					tr := tar.NewReader(gr)
143
144					hdr, err := tr.Next()
145					Expect(err).NotTo(HaveOccurred())
146
147					Expect(hdr.Name).To(Equal("./"))
148
149					hdr, err = tr.Next()
150					Expect(err).NotTo(HaveOccurred())
151
152					Expect(hdr.Name).To(MatchRegexp("(./)?task.yml$"))
153
154					uploadedBits <- struct{}{}
155				},
156				ghttp.RespondWith(201, `{"id":125}`),
157			),
158		)
159		atcServer.RouteToHandler("POST", "/api/v1/teams/main/builds",
160			ghttp.CombineHandlers(
161				ghttp.VerifyRequest("POST", "/api/v1/teams/main/builds"),
162				VerifyPlan(expectedPlan),
163				func(w http.ResponseWriter, r *http.Request) {
164					http.SetCookie(w, &http.Cookie{
165						Name:    "Some-Cookie",
166						Value:   "some-cookie-data",
167						Path:    "/",
168						Expires: time.Now().Add(1 * time.Minute),
169					})
170				},
171				ghttp.RespondWith(201, `{"id":128}`),
172			),
173		)
174		atcServer.RouteToHandler("GET", "/api/v1/builds/128/events",
175			ghttp.CombineHandlers(
176				ghttp.VerifyRequest("GET", "/api/v1/builds/128/events"),
177				func(w http.ResponseWriter, r *http.Request) {
178					flusher := w.(http.Flusher)
179
180					w.Header().Add("Content-Type", "text/event-stream; charset=utf-8")
181					w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
182					w.Header().Add("Connection", "keep-alive")
183
184					w.WriteHeader(http.StatusOK)
185
186					flusher.Flush()
187
188					close(streaming)
189
190					id := 0
191
192					for e := range events {
193						payload, err := json.Marshal(event.Message{Event: e})
194						Expect(err).NotTo(HaveOccurred())
195
196						event := sse.Event{
197							ID:   fmt.Sprintf("%d", id),
198							Name: "event",
199							Data: payload,
200						}
201
202						err = event.Write(w)
203						Expect(err).NotTo(HaveOccurred())
204
205						flusher.Flush()
206
207						id++
208					}
209
210					err := sse.Event{
211						Name: "end",
212					}.Write(w)
213					Expect(err).NotTo(HaveOccurred())
214				},
215			),
216		)
217		atcServer.RouteToHandler("GET", "/api/v1/builds/128/artifacts",
218			ghttp.RespondWithJSONEncoded(200, []atc.WorkerArtifact{workerArtifact}),
219		)
220
221	})
222
223	It("creates a build, streams output, uploads the bits, and polls until completion", func() {
224		flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
225		flyCmd.Dir = buildDir
226
227		sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
228		Expect(err).NotTo(HaveOccurred())
229
230		Eventually(streaming).Should(BeClosed())
231
232		buildURL, _ := url.Parse(atcServer.URL())
233		buildURL.Path = path.Join(buildURL.Path, "builds/128")
234		Eventually(sess.Out).Should(gbytes.Say("executing build 128 at %s", buildURL.String()))
235
236		events <- event.Log{Payload: "sup"}
237
238		Eventually(sess.Out).Should(gbytes.Say("sup"))
239
240		close(events)
241
242		<-sess.Exited
243		Expect(sess.ExitCode()).To(Equal(0))
244
245		Expect(uploadedBits).To(HaveLen(1))
246	})
247
248	Context("when there is a pipeline job with the same input", func() {
249		BeforeEach(func() {
250			taskPlan.Task.VersionedResourceTypes = atc.VersionedResourceTypes{
251				atc.VersionedResourceType{
252					ResourceType: atc.ResourceType{
253						Name:   "resource-type",
254						Type:   "s3",
255						Source: atc.Source{},
256					},
257				},
258			}
259
260			planFactory := atc.NewPlanFactory(0)
261
262			expectedPlan = planFactory.NewPlan(atc.DoPlan{
263				planFactory.NewPlan(atc.AggregatePlan{
264					planFactory.NewPlan(atc.GetPlan{
265						Name: "fixture",
266						VersionedResourceTypes: atc.VersionedResourceTypes{
267							atc.VersionedResourceType{
268								ResourceType: atc.ResourceType{
269									Name:   "resource-type",
270									Type:   "s3",
271									Source: atc.Source{},
272								},
273							},
274						},
275					}),
276				}),
277				taskPlan,
278			})
279
280			atcServer.RouteToHandler("POST", "/api/v1/teams/main/pipelines/some-pipeline/builds",
281				ghttp.CombineHandlers(
282					ghttp.VerifyRequest("POST", "/api/v1/teams/main/pipelines/some-pipeline/builds"),
283					testhelpers.VerifyPlan(expectedPlan),
284					func(w http.ResponseWriter, r *http.Request) {
285						http.SetCookie(w, &http.Cookie{
286							Name:    "Some-Cookie",
287							Value:   "some-cookie-data",
288							Path:    "/",
289							Expires: time.Now().Add(1 * time.Minute),
290						})
291					},
292					ghttp.RespondWith(201, `{"id":128}`),
293				),
294			)
295			atcServer.RouteToHandler("GET", "/api/v1/teams/main/pipelines/some-pipeline/jobs/some-job/inputs",
296				ghttp.RespondWithJSONEncoded(200, []atc.BuildInput{atc.BuildInput{Name: "fixture"}}),
297			)
298			atcServer.RouteToHandler("GET", "/api/v1/teams/main/pipelines/some-pipeline/resource-types",
299				ghttp.RespondWithJSONEncoded(200, atc.VersionedResourceTypes{
300					atc.VersionedResourceType{
301						ResourceType: atc.ResourceType{
302							Name:   "resource-type",
303							Type:   "s3",
304							Source: atc.Source{},
305						},
306					},
307				}),
308			)
309		})
310
311		It("creates a build, streams output, and polls until completion", func() {
312			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-j", "some-pipeline/some-job")
313			flyCmd.Dir = buildDir
314
315			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
316			Expect(err).NotTo(HaveOccurred())
317
318			Eventually(streaming).Should(BeClosed())
319
320			buildURL, _ := url.Parse(atcServer.URL())
321			buildURL.Path = path.Join(buildURL.Path, "builds/128")
322			Eventually(sess.Out).Should(gbytes.Say("executing build 128 at %s", buildURL.String()))
323
324			events <- event.Log{Payload: "sup"}
325
326			Eventually(sess.Out).Should(gbytes.Say("sup"))
327
328			close(events)
329
330			<-sess.Exited
331			Expect(sess.ExitCode()).To(Equal(0))
332		})
333
334	})
335
336	Context("when the build config is invalid", func() {
337		BeforeEach(func() {
338			// missing platform and run path
339			err := ioutil.WriteFile(
340				filepath.Join(buildDir, "task.yml"),
341				[]byte(`---
342run: {}
343`),
344				0644,
345			)
346			Expect(err).NotTo(HaveOccurred())
347		})
348
349		It("prints the failure and exits 1", func() {
350			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
351			flyCmd.Dir = buildDir
352
353			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
354			Expect(err).NotTo(HaveOccurred())
355
356			Eventually(sess.Err).Should(gbytes.Say("missing"))
357
358			<-sess.Exited
359			Expect(sess.ExitCode()).To(Equal(1))
360		})
361	})
362
363	Context("when the build config is valid", func() {
364		JustBeforeEach(func() {
365			atcServer.RouteToHandler("POST", "/api/v1/teams/main/artifacts",
366				ghttp.CombineHandlers(
367					func(w http.ResponseWriter, req *http.Request) {
368						uploadedBits <- struct{}{}
369					},
370					ghttp.RespondWith(201, `{"id":125}`),
371				),
372			)
373		})
374
375		Context("when task defines one input but it was not passed in as a flag", func() {
376			It("uploads the current directory", func() {
377				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
378				flyCmd.Dir = buildDir
379
380				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
381				Expect(err).NotTo(HaveOccurred())
382
383				buildURL, _ := url.Parse(atcServer.URL())
384				buildURL.Path = path.Join(buildURL.Path, "builds/128")
385				Eventually(sess.Out).Should(gbytes.Say("executing build 128 at %s", buildURL.String()))
386
387				close(events)
388
389				<-sess.Exited
390				Expect(sess.ExitCode()).To(Equal(0))
391
392				Expect(uploadedBits).To(HaveLen(1))
393			})
394		})
395
396		Context("when task defines 2 inputs but only 1 was passed as a flag", func() {
397			var bardir string
398
399			BeforeEach(func() {
400				err := ioutil.WriteFile(
401					filepath.Join(buildDir, "task.yml"),
402					[]byte(`---
403platform: some-platform
404
405image_resource:
406  type: registry-image
407  source:
408    repository: ubuntu
409
410inputs:
411- name: fixture
412- name: bar
413
414run:
415  path: find
416  args: [.]
417`),
418					0644,
419				)
420				bardir = filepath.Join(tmpdir, "bar")
421				err = os.Mkdir(bardir, 0755)
422				Expect(err).ToNot(HaveOccurred())
423
424				taskPlan.Task.Config.Inputs = []atc.TaskInputConfig{
425					{Name: "fixture"},
426					{Name: "bar"},
427				}
428				taskPlan.Task.Config.Params = nil
429
430				Expect(err).NotTo(HaveOccurred())
431				expectedPlan = planFactory.NewPlan(atc.DoPlan{
432					planFactory.NewPlan(atc.AggregatePlan{
433						planFactory.NewPlan(atc.ArtifactInputPlan{
434							ArtifactID: 125,
435							Name:       filepath.Base(buildDir),
436						}),
437						planFactory.NewPlan(atc.ArtifactInputPlan{
438							ArtifactID: 125,
439							Name:       filepath.Base(bardir),
440						}),
441					}),
442					taskPlan,
443				})
444
445			})
446
447			AfterEach(func() {
448				os.RemoveAll(bardir)
449			})
450
451			Context("when the current directory name is the same as the missing input", func() {
452				It("uploads the current directory", func() {
453					flyCmd := exec.Command(flyPath, "-t", targetName, "e",
454						"-c", taskConfigPath,
455						"-i", fmt.Sprintf("bar=%s", bardir),
456					)
457					flyCmd.Dir = buildDir
458
459					sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
460					Expect(err).NotTo(HaveOccurred())
461
462					buildURL, _ := url.Parse(atcServer.URL())
463					buildURL.Path = path.Join(buildURL.Path, "builds/128")
464					Eventually(sess.Out).Should(gbytes.Say("executing build 128 at %s", buildURL.String()))
465
466					close(events)
467
468					<-sess.Exited
469					Expect(sess.ExitCode()).To(Equal(0))
470
471					Expect(uploadedBits).To(HaveLen(2))
472				})
473			})
474
475			Context("when the current directory name is not the same as the missing input", func() {
476				BeforeEach(func() {
477					err := ioutil.WriteFile(
478						filepath.Join(buildDir, "task.yml"),
479						[]byte(`---
480platform: some-platform
481
482image_resource:
483  type: registry-image
484  source:
485    repository: ubuntu
486
487inputs:
488- name: foo
489- name: bar
490
491params:
492  FOO: bar
493  BAZ: buzz
494  X: 1
495  EMPTY:
496
497run:
498  path: find
499  args: [.]
500`),
501						0644,
502					)
503					Expect(err).NotTo(HaveOccurred())
504					(*expectedPlan.Do)[1].Task.Config.Inputs = []atc.TaskInputConfig{
505						{Name: "foo"},
506						{Name: "bar"},
507					}
508				})
509
510				It("errors with the missing input", func() {
511					flyCmd := exec.Command(flyPath, "-t", targetName, "e",
512						"-c", taskConfigPath,
513						"-i", fmt.Sprintf("bar=%s", bardir),
514					)
515					flyCmd.Dir = buildDir
516
517					sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
518					Expect(err).NotTo(HaveOccurred())
519
520					Eventually(sess.Err).Should(gbytes.Say("error: missing required input `foo`"))
521
522					close(events)
523
524					<-sess.Exited
525					Expect(sess.ExitCode()).To(Equal(1))
526
527					Expect(uploadedBits).To(HaveLen(1))
528				})
529			})
530		})
531	})
532
533	Context("when arguments include input that is not a git repo", func() {
534
535		Context("when arguments not include --include-ignored", func() {
536			It("uploading with everything", func() {
537				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-i", "fixture="+buildDir)
538
539				flyCmd.Dir = buildDir
540
541				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
542				Expect(err).NotTo(HaveOccurred())
543
544				// sync with after create
545				Eventually(streaming).Should(BeClosed())
546
547				close(events)
548
549				<-sess.Exited
550				Expect(sess.ExitCode()).To(Equal(0))
551
552				Expect(uploadedBits).To(HaveLen(1))
553			})
554		})
555	})
556
557	Context("when arguments include input that is a git repo", func() {
558
559		BeforeEach(func() {
560			gitIgnorePath := filepath.Join(buildDir, ".gitignore")
561
562			err := ioutil.WriteFile(gitIgnorePath, []byte(`*.test`), 0644)
563			Expect(err).NotTo(HaveOccurred())
564
565			fileToBeIgnoredPath := filepath.Join(buildDir, "dev.test")
566			err = ioutil.WriteFile(fileToBeIgnoredPath, []byte(`test file content`), 0644)
567			Expect(err).NotTo(HaveOccurred())
568
569			err = os.Mkdir(filepath.Join(buildDir, ".git"), 0755)
570			Expect(err).NotTo(HaveOccurred())
571
572			err = os.Mkdir(filepath.Join(buildDir, ".git/refs"), 0755)
573			Expect(err).NotTo(HaveOccurred())
574
575			err = os.Mkdir(filepath.Join(buildDir, ".git/objects"), 0755)
576			Expect(err).NotTo(HaveOccurred())
577
578			gitHEADPath := filepath.Join(buildDir, ".git/HEAD")
579			err = ioutil.WriteFile(gitHEADPath, []byte(`ref: refs/heads/master`), 0644)
580			Expect(err).NotTo(HaveOccurred())
581		})
582
583		Context("when arguments not include --include-ignored", func() {
584			It("by default apply .gitignore", func() {
585				atcServer.RouteToHandler("POST", "/api/v1/teams/main/artifacts",
586					ghttp.CombineHandlers(
587						func(w http.ResponseWriter, req *http.Request) {
588							gr, err := gzip.NewReader(req.Body)
589							Expect(err).NotTo(HaveOccurred())
590
591							tr := tar.NewReader(gr)
592
593							var matchFound = false
594							for {
595								hdr, err := tr.Next()
596								if err != nil {
597									break
598								}
599								if strings.Contains(hdr.Name, "dev.test") {
600									matchFound = true
601									break
602								}
603							}
604
605							Expect(matchFound).To(Equal(false))
606
607							uploadedBits <- struct{}{}
608						},
609						ghttp.RespondWith(201, `{"id":125}`),
610					),
611				)
612				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
613				flyCmd.Dir = buildDir
614
615				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
616				Expect(err).NotTo(HaveOccurred())
617
618				// sync with after create
619				Eventually(streaming).Should(BeClosed())
620
621				close(events)
622
623				<-sess.Exited
624				Expect(sess.ExitCode()).To(Equal(0))
625
626				Expect(uploadedBits).To(HaveLen(1))
627			})
628		})
629
630		Context("when arguments include --include-ignored", func() {
631			It("uploading with everything", func() {
632				atcServer.RouteToHandler("POST", "/api/v1/teams/main/artifacts",
633					ghttp.CombineHandlers(
634						func(w http.ResponseWriter, req *http.Request) {
635							Expect(req.FormValue("platform")).To(Equal("some-platform"))
636
637							gr, err := gzip.NewReader(req.Body)
638							Expect(err).NotTo(HaveOccurred())
639
640							tr := tar.NewReader(gr)
641
642							var matchFound = false
643							for {
644								hdr, err := tr.Next()
645								if err != nil {
646									break
647								}
648								if strings.Contains(hdr.Name, "dev.test") {
649									matchFound = true
650									break
651								}
652							}
653
654							Expect(matchFound).To(Equal(true))
655							uploadedBits <- struct{}{}
656						},
657						ghttp.RespondWith(201, `{"id":125}`),
658					),
659				)
660
661				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "--include-ignored")
662				flyCmd.Dir = buildDir
663
664				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
665				Expect(err).NotTo(HaveOccurred())
666
667				// sync with after create
668				Eventually(streaming).Should(BeClosed())
669
670				close(events)
671
672				<-sess.Exited
673				Expect(sess.ExitCode()).To(Equal(0))
674
675				Expect(uploadedBits).To(HaveLen(1))
676			})
677		})
678	})
679
680	Context("when arguments are passed through", func() {
681		BeforeEach(func() {
682			(*expectedPlan.Do)[1].Task.Config.Run.Args = []string{".", "-name", `foo "bar" baz`}
683		})
684
685		It("inserts them into the config template", func() {
686			atcServer.AllowUnhandledRequests = true
687
688			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "--", "-name", "foo \"bar\" baz")
689			flyCmd.Dir = buildDir
690
691			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
692			Expect(err).NotTo(HaveOccurred())
693
694			// sync with after create
695			Eventually(streaming).Should(BeClosed())
696
697			close(events)
698
699			<-sess.Exited
700			Expect(sess.ExitCode()).To(Equal(0))
701
702			Expect(uploadedBits).To(HaveLen(1))
703		})
704	})
705
706	Context("when tags are specified", func() {
707		BeforeEach(func() {
708			(*expectedPlan.Do)[1].Task.Tags = []string{"tag-1", "tag-2"}
709		})
710
711		It("sprinkles them on the task", func() {
712			atcServer.AllowUnhandledRequests = true
713
714			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "--tag", "tag-1", "--tag", "tag-2")
715			flyCmd.Dir = buildDir
716
717			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
718			Expect(err).NotTo(HaveOccurred())
719
720			// sync with after create
721			Eventually(streaming).Should(BeClosed())
722
723			close(events)
724
725			<-sess.Exited
726			Expect(sess.ExitCode()).To(Equal(0))
727
728			Expect(uploadedBits).To(HaveLen(1))
729		})
730	})
731
732	Context("when invalid inputs are passed", func() {
733		It("prints an error", func() {
734			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-i", "fixture=.", "-i", "evan=.")
735			flyCmd.Dir = buildDir
736
737			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
738			Expect(err).NotTo(HaveOccurred())
739
740			Eventually(sess.Err).Should(gbytes.Say("unknown input `evan`"))
741
742			<-sess.Exited
743			Expect(sess.ExitCode()).To(Equal(1))
744		})
745
746		Context("when input is not a folder", func() {
747			It("prints an error", func() {
748				testFile := filepath.Join(buildDir, "test-file.txt")
749				err := ioutil.WriteFile(
750					testFile,
751					[]byte(`test file content`),
752					0644,
753				)
754				Expect(err).NotTo(HaveOccurred())
755
756				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-i", "fixture=./test-file.txt")
757				flyCmd.Dir = buildDir
758
759				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
760				Expect(err).NotTo(HaveOccurred())
761
762				Eventually(sess.Err).Should(gbytes.Say("./test-file.txt not a folder"))
763
764				<-sess.Exited
765				Expect(sess.ExitCode()).To(Equal(1))
766			})
767		})
768
769		Context("when invalid inputs are passed and the single valid input is correctly omitted", func() {
770			It("prints an error about invalid inputs instead of missing inputs", func() {
771				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-i", "evan=.")
772				flyCmd.Dir = buildDir
773
774				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
775				Expect(err).NotTo(HaveOccurred())
776
777				Eventually(sess.Err).Should(gbytes.Say("unknown input `evan`"))
778
779				<-sess.Exited
780				Expect(sess.ExitCode()).To(Equal(1))
781			})
782		})
783	})
784
785	Context("when the task specifies no input", func() {
786		BeforeEach(func() {
787			err := ioutil.WriteFile(
788				filepath.Join(buildDir, "task.yml"),
789				[]byte(`---
790platform: some-platform
791
792image_resource:
793  type: registry-image
794  source:
795    repository: ubuntu
796
797inputs:
798
799params:
800  FOO: bar
801  BAZ: buzz
802  X: 1
803  EMPTY:
804
805
806run:
807  path: find
808  args: [.]
809`),
810				0644,
811			)
812			Expect(err).NotTo(HaveOccurred())
813			(*expectedPlan.Do)[1].Task.Config.Inputs = nil
814			(*expectedPlan.Do)[0].Aggregate = &atc.AggregatePlan{}
815		})
816
817		It("shouldn't upload the current directory", func() {
818			atcServer.RouteToHandler("POST", "/api/v1/teams/main/artifacts",
819				ghttp.CombineHandlers(
820					func(w http.ResponseWriter, req *http.Request) {
821						uploadedBits <- struct{}{}
822					},
823					ghttp.RespondWith(201, `{"id":125}`),
824				),
825			)
826
827			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
828			flyCmd.Dir = buildDir
829
830			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
831			Expect(err).NotTo(HaveOccurred())
832
833			close(events)
834
835			<-sess.Exited
836			Expect(sess.ExitCode()).To(Equal(0))
837			Expect(uploadedBits).To(HaveLen(0))
838		})
839	})
840
841	Context("when the task specifies an optional input", func() {
842		BeforeEach(func() {
843			err := ioutil.WriteFile(
844				filepath.Join(buildDir, "task.yml"),
845				[]byte(`---
846platform: some-platform
847
848image_resource:
849  type: registry-image
850  source:
851    repository: ubuntu
852
853inputs:
854- name: fixture
855- name: some-optional-input
856  optional: true
857
858params:
859  FOO: bar
860  BAZ: buzz
861  X: 1
862  EMPTY:
863
864run:
865  path: find
866  args: [.]
867`),
868				0644,
869			)
870			Expect(err).NotTo(HaveOccurred())
871			(*expectedPlan.Do)[1].Task.Config.Inputs = []atc.TaskInputConfig{
872				{Name: "fixture"},
873				{Name: "some-optional-input", Optional: true},
874			}
875		})
876
877		Context("when the required input is specified but the optional input is omitted", func() {
878			It("runs successfully", func() {
879				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-i", "fixture=.")
880				flyCmd.Dir = buildDir
881
882				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
883				Expect(err).NotTo(HaveOccurred())
884
885				Eventually(streaming).Should(BeClosed())
886
887				buildURL, _ := url.Parse(atcServer.URL())
888				buildURL.Path = path.Join(buildURL.Path, "builds/128")
889				Eventually(sess.Out).Should(gbytes.Say("executing build 128 at %s", buildURL.String()))
890
891				events <- event.Log{Payload: "sup"}
892
893				Eventually(sess.Out).Should(gbytes.Say("sup"))
894
895				close(events)
896
897				<-sess.Exited
898				Expect(sess.ExitCode()).To(Equal(0))
899
900				Expect(uploadedBits).To(HaveLen(1))
901			})
902		})
903
904		Context("when the required input is not specified on the command line", func() {
905			It("runs infers the required input successfully", func() {
906				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
907				flyCmd.Dir = buildDir
908
909				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
910				Expect(err).NotTo(HaveOccurred())
911
912				Eventually(streaming).Should(BeClosed())
913
914				buildURL, _ := url.Parse(atcServer.URL())
915				buildURL.Path = path.Join(buildURL.Path, "builds/128")
916				Eventually(sess.Out).Should(gbytes.Say("executing build 128 at %s", buildURL.String()))
917
918				events <- event.Log{Payload: "sup"}
919
920				Eventually(sess.Out).Should(gbytes.Say("sup"))
921
922				close(events)
923
924				<-sess.Exited
925				Expect(sess.ExitCode()).To(Equal(0))
926
927				Expect(uploadedBits).To(HaveLen(1))
928			})
929		})
930	})
931
932	Context("when the task specifies more than one required input", func() {
933		BeforeEach(func() {
934			err := ioutil.WriteFile(
935				filepath.Join(buildDir, "task.yml"),
936				[]byte(`---
937platform: some-platform
938
939image_resource:
940  type: registry-image
941  source:
942    repository: ubuntu
943
944inputs:
945- name: fixture
946- name: something
947
948params:
949  FOO: bar
950  BAZ: buzz
951  X: 1
952  EMPTY:
953
954run:
955  path: find
956  args: [.]
957`),
958				0644,
959			)
960			Expect(err).NotTo(HaveOccurred())
961		})
962
963		Context("When some required inputs are not passed", func() {
964			It("Prints an error", func() {
965				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-i", "fixture=.")
966				flyCmd.Dir = buildDir
967
968				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
969				Expect(err).NotTo(HaveOccurred())
970
971				Eventually(sess.Err).Should(gbytes.Say("missing required input `something`"))
972
973				<-sess.Exited
974				Expect(sess.ExitCode()).To(Equal(1))
975			})
976
977		})
978
979		Context("When no inputs are passed", func() {
980			It("Prints an error", func() {
981				flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
982				flyCmd.Dir = buildDir
983
984				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
985				Expect(err).NotTo(HaveOccurred())
986
987				Eventually(sess.Err).Should(gbytes.Say("missing required input"))
988
989				<-sess.Exited
990				Expect(sess.ExitCode()).To(Equal(1))
991			})
992		})
993	})
994
995	Context("when running with --privileged", func() {
996		BeforeEach(func() {
997			(*expectedPlan.Do)[1].Task.Privileged = true
998		})
999
1000		It("inserts them into the config template", func() {
1001			atcServer.AllowUnhandledRequests = true
1002
1003			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "--privileged")
1004			flyCmd.Dir = buildDir
1005
1006			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1007			Expect(err).NotTo(HaveOccurred())
1008
1009			// sync with after create
1010			Eventually(streaming).Should(BeClosed())
1011
1012			close(events)
1013
1014			<-sess.Exited
1015			Expect(sess.ExitCode()).To(Equal(0))
1016
1017			Expect(uploadedBits).To(HaveLen(1))
1018		})
1019	})
1020
1021	Context("when running with bogus flags", func() {
1022		It("exits 1", func() {
1023			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "--bogus-flag")
1024			flyCmd.Dir = buildDir
1025
1026			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1027			Expect(err).NotTo(HaveOccurred())
1028
1029			Eventually(sess.Err).Should(gbytes.Say("unknown flag `bogus-flag'"))
1030
1031			<-sess.Exited
1032			Expect(sess.ExitCode()).To(Equal(1))
1033		})
1034	})
1035
1036	Context("when running with invalid -j flag", func() {
1037		It("exits 1", func() {
1038			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath, "-j", "some-pipeline/invalid/some-job")
1039			flyCmd.Dir = buildDir
1040
1041			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1042			Expect(err).NotTo(HaveOccurred())
1043
1044			Eventually(sess.Err).Should(gbytes.Say("argument format should be <pipeline>/<job>"))
1045
1046			<-sess.Exited
1047			Expect(sess.ExitCode()).To(Equal(1))
1048		})
1049	})
1050
1051	Context("when parameters are specified in the environment", func() {
1052		BeforeEach(func() {
1053			(*expectedPlan.Do)[1].Task.Config.Params = map[string]string{
1054				"FOO":   "newbar",
1055				"BAZ":   "buzz",
1056				"X":     "",
1057				"EMPTY": "",
1058			}
1059		})
1060
1061		It("overrides the builds parameter values", func() {
1062			atcServer.AllowUnhandledRequests = true
1063
1064			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
1065			flyCmd.Dir = buildDir
1066			flyCmd.Env = append(os.Environ(), "FOO=newbar", "X=")
1067
1068			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1069			Expect(err).NotTo(HaveOccurred())
1070
1071			// sync with after create
1072			Eventually(streaming).Should(BeClosed())
1073
1074			close(events)
1075
1076			<-sess.Exited
1077			Expect(sess.ExitCode()).To(Equal(0))
1078
1079			Expect(uploadedBits).To(HaveLen(1))
1080		})
1081	})
1082
1083	Context("when the build is interrupted", func() {
1084		var aborted chan struct{}
1085
1086		JustBeforeEach(func() {
1087			aborted = make(chan struct{})
1088
1089			atcServer.AppendHandlers(
1090				ghttp.CombineHandlers(
1091					ghttp.VerifyRequest("PUT", "/api/v1/builds/128/abort"),
1092					func(w http.ResponseWriter, r *http.Request) {
1093						close(aborted)
1094					},
1095				),
1096			)
1097		})
1098
1099		if runtime.GOOS != "windows" {
1100			Describe("with SIGINT", func() {
1101				It("aborts the build and exits nonzero", func() {
1102					flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
1103					flyCmd.Dir = buildDir
1104
1105					sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1106					Expect(err).ToNot(HaveOccurred())
1107
1108					Eventually(streaming).Should(BeClosed())
1109
1110					Expect(uploadedBits).To(HaveLen(1))
1111
1112					sess.Signal(os.Interrupt)
1113
1114					Eventually(aborted).Should(BeClosed())
1115
1116					events <- event.Status{Status: atc.StatusErrored}
1117					close(events)
1118
1119					<-sess.Exited
1120					Expect(sess.ExitCode()).To(Equal(2))
1121				})
1122			})
1123
1124			Describe("with SIGTERM", func() {
1125				It("aborts the build and exits nonzero", func() {
1126					flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
1127					flyCmd.Dir = buildDir
1128
1129					sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1130					Expect(err).ToNot(HaveOccurred())
1131
1132					Eventually(streaming).Should(BeClosed())
1133
1134					Expect(uploadedBits).To(HaveLen(1))
1135
1136					sess.Signal(syscall.SIGTERM)
1137
1138					Eventually(aborted).Should(BeClosed())
1139
1140					events <- event.Status{Status: atc.StatusErrored}
1141					close(events)
1142
1143					<-sess.Exited
1144					Expect(sess.ExitCode()).To(Equal(2))
1145				})
1146			})
1147		}
1148	})
1149
1150	Context("when the build succeeds", func() {
1151		It("exits 0", func() {
1152			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
1153			flyCmd.Dir = buildDir
1154
1155			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1156			Expect(err).ToNot(HaveOccurred())
1157
1158			Eventually(streaming).Should(BeClosed())
1159
1160			events <- event.Status{Status: atc.StatusSucceeded}
1161			close(events)
1162
1163			<-sess.Exited
1164			Expect(sess.ExitCode()).To(Equal(0))
1165
1166			Expect(uploadedBits).To(HaveLen(1))
1167		})
1168	})
1169
1170	Context("when the build fails", func() {
1171		It("exits 1", func() {
1172			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
1173			flyCmd.Dir = buildDir
1174
1175			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1176			Expect(err).ToNot(HaveOccurred())
1177
1178			Eventually(streaming).Should(BeClosed())
1179
1180			events <- event.Status{Status: atc.StatusFailed}
1181			close(events)
1182
1183			<-sess.Exited
1184			Expect(sess.ExitCode()).To(Equal(1))
1185
1186			Expect(uploadedBits).To(HaveLen(1))
1187		})
1188	})
1189
1190	Context("when the build errors", func() {
1191		It("exits 2", func() {
1192			flyCmd := exec.Command(flyPath, "-t", targetName, "e", "-c", taskConfigPath)
1193			flyCmd.Dir = buildDir
1194
1195			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
1196			Expect(err).ToNot(HaveOccurred())
1197
1198			Eventually(streaming).Should(BeClosed())
1199
1200			events <- event.Status{Status: atc.StatusErrored}
1201			close(events)
1202
1203			<-sess.Exited
1204			Expect(sess.ExitCode()).To(Equal(2))
1205
1206			Expect(uploadedBits).To(HaveLen(1))
1207		})
1208	})
1209})
1210