github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/fly/integration/hijack_test.go (about)

     1  package integration_test
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  
    10  	"github.com/pf-qiu/concourse/v6/atc"
    11  	"github.com/gorilla/websocket"
    12  	"github.com/mgutz/ansi"
    13  	. "github.com/onsi/ginkgo"
    14  	. "github.com/onsi/gomega"
    15  	"github.com/onsi/gomega/gbytes"
    16  	"github.com/onsi/gomega/gexec"
    17  	"github.com/onsi/gomega/ghttp"
    18  )
    19  
    20  var _ = Describe("Hijacking", func() {
    21  	var hijacked <-chan struct{}
    22  	var workingDirectory string
    23  	var user string
    24  	var path string
    25  	var args []string
    26  
    27  	BeforeEach(func() {
    28  		hijacked = nil
    29  		workingDirectory = ""
    30  		user = "root"
    31  		path = "bash"
    32  		args = nil
    33  	})
    34  
    35  	upgrader := websocket.Upgrader{}
    36  
    37  	hijackHandler := func(id string, didHijack chan<- struct{}, errorMessages []string, teamName string) http.HandlerFunc {
    38  		return ghttp.CombineHandlers(
    39  			ghttp.VerifyRequest("GET", fmt.Sprintf("/api/v1/teams/"+teamName+"/containers/%s/hijack", id)),
    40  			func(w http.ResponseWriter, r *http.Request) {
    41  				defer GinkgoRecover()
    42  
    43  				conn, err := upgrader.Upgrade(w, r, nil)
    44  				Expect(err).NotTo(HaveOccurred())
    45  
    46  				defer conn.Close()
    47  
    48  				close(didHijack)
    49  
    50  				var processSpec atc.HijackProcessSpec
    51  				err = conn.ReadJSON(&processSpec)
    52  				Expect(err).NotTo(HaveOccurred())
    53  
    54  				Expect(processSpec.User).To(Equal(user))
    55  				Expect(processSpec.Dir).To(Equal(workingDirectory))
    56  				Expect(processSpec.Path).To(Equal(path))
    57  				Expect(processSpec.Args).To(Equal(args))
    58  
    59  				var payload atc.HijackInput
    60  				err = conn.ReadJSON(&payload)
    61  				Expect(err).NotTo(HaveOccurred())
    62  				Expect(payload).To(Equal(atc.HijackInput{
    63  					Stdin: []byte("some stdin"),
    64  				}))
    65  
    66  				err = conn.WriteJSON(atc.HijackOutput{
    67  					Stdout: []byte("some stdout"),
    68  				})
    69  				Expect(err).NotTo(HaveOccurred())
    70  
    71  				err = conn.WriteJSON(atc.HijackOutput{
    72  					Stderr: []byte("some stderr"),
    73  				})
    74  				Expect(err).NotTo(HaveOccurred())
    75  
    76  				if len(errorMessages) > 0 {
    77  					for _, msg := range errorMessages {
    78  						err := conn.WriteJSON(atc.HijackOutput{
    79  							Error: msg,
    80  						})
    81  						Expect(err).NotTo(HaveOccurred())
    82  					}
    83  
    84  					return
    85  				}
    86  
    87  				var closePayload atc.HijackInput
    88  				err = conn.ReadJSON(&closePayload)
    89  				Expect(err).NotTo(HaveOccurred())
    90  				Expect(closePayload).To(Equal(atc.HijackInput{
    91  					Closed: true,
    92  				}))
    93  
    94  				exitStatus := 123
    95  				err = conn.WriteJSON(atc.HijackOutput{
    96  					ExitStatus: &exitStatus,
    97  				})
    98  				Expect(err).NotTo(HaveOccurred())
    99  			},
   100  		)
   101  	}
   102  
   103  	fly := func(command string, args ...string) {
   104  		commandWithArgs := append([]string{command}, args...)
   105  
   106  		flyCmd := exec.Command(flyPath, append([]string{"-t", targetName}, commandWithArgs...)...)
   107  
   108  		stdin, err := flyCmd.StdinPipe()
   109  		Expect(err).NotTo(HaveOccurred())
   110  
   111  		sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   112  		Expect(err).NotTo(HaveOccurred())
   113  
   114  		Eventually(hijacked).Should(BeClosed())
   115  
   116  		_, err = fmt.Fprintf(stdin, "some stdin")
   117  		Expect(err).NotTo(HaveOccurred())
   118  
   119  		Eventually(sess.Out).Should(gbytes.Say("some stdout"))
   120  		Eventually(sess.Err).Should(gbytes.Say("some stderr"))
   121  
   122  		err = stdin.Close()
   123  		Expect(err).NotTo(HaveOccurred())
   124  
   125  		<-sess.Exited
   126  		Expect(sess.ExitCode()).To(Equal(123))
   127  	}
   128  
   129  	hijack := func(args ...string) {
   130  		fly("hijack", args...)
   131  	}
   132  
   133  	Context("with only a step name specified", func() {
   134  		BeforeEach(func() {
   135  			didHijack := make(chan struct{})
   136  			hijacked = didHijack
   137  
   138  			atcServer.AppendHandlers(
   139  				ghttp.CombineHandlers(
   140  					ghttp.VerifyRequest("GET", "/api/v1/builds"),
   141  					ghttp.RespondWithJSONEncoded(200, []atc.Build{
   142  						{ID: 4, Name: "1", Status: "started", JobName: "some-job"},
   143  						{ID: 3, Name: "3", Status: "started"},
   144  						{ID: 2, Name: "2", Status: "started"},
   145  						{ID: 1, Name: "1", Status: "finished"},
   146  					}),
   147  				),
   148  				ghttp.CombineHandlers(
   149  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers", "build_id=3&step_name=some-step"),
   150  					ghttp.RespondWithJSONEncoded(200, []atc.Container{
   151  						{ID: "container-id-1", State: atc.ContainerStateCreated, BuildID: 3, Type: "task", StepName: "some-step", User: user},
   152  					}),
   153  				),
   154  				hijackHandler("container-id-1", didHijack, nil, "main"),
   155  			)
   156  		})
   157  
   158  		It("hijacks the most recent one-off build", func() {
   159  			hijack("-s", "some-step")
   160  		})
   161  
   162  		It("hijacks the most recent one-off build with a more politically correct command", func() {
   163  			fly("intercept", "-s", "some-step")
   164  		})
   165  	})
   166  
   167  	Context("when the container specifies a working directory", func() {
   168  		BeforeEach(func() {
   169  			didHijack := make(chan struct{})
   170  			hijacked = didHijack
   171  			workingDirectory = "/tmp/build/my-favorite-guid"
   172  
   173  			atcServer.AppendHandlers(
   174  				ghttp.CombineHandlers(
   175  					ghttp.VerifyRequest("GET", "/api/v1/builds"),
   176  					ghttp.RespondWithJSONEncoded(200, []atc.Build{
   177  						{ID: 3, Name: "3", Status: "started"},
   178  					}),
   179  				),
   180  				ghttp.CombineHandlers(
   181  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers", "build_id=3&step_name=some-step"),
   182  					ghttp.RespondWithJSONEncoded(200, []atc.Container{
   183  						{ID: "container-id-1", State: atc.ContainerStateCreated, BuildID: 3, Type: "task", StepName: "some-step", WorkingDirectory: workingDirectory, User: user},
   184  					}),
   185  				),
   186  				hijackHandler("container-id-1", didHijack, nil, "main"),
   187  			)
   188  		})
   189  
   190  		It("hijacks the most recent one-off build in the specified working directory", func() {
   191  			hijack("-s", "some-step")
   192  		})
   193  	})
   194  
   195  	Context("when the container specifies a user", func() {
   196  		BeforeEach(func() {
   197  			didHijack := make(chan struct{})
   198  			hijacked = didHijack
   199  			user = "amelia"
   200  
   201  			atcServer.AppendHandlers(
   202  				ghttp.CombineHandlers(
   203  					ghttp.VerifyRequest("GET", "/api/v1/builds"),
   204  					ghttp.RespondWithJSONEncoded(200, []atc.Build{
   205  						{ID: 3, Name: "3", Status: "started"},
   206  					}),
   207  				),
   208  				ghttp.CombineHandlers(
   209  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers", "build_id=3&step_name=some-step"),
   210  					ghttp.RespondWithJSONEncoded(200, []atc.Container{
   211  						{ID: "container-id-1", State: atc.ContainerStateCreated, BuildID: 3, Type: "task", StepName: "some-step", User: "amelia"},
   212  					}),
   213  				),
   214  				hijackHandler("container-id-1", didHijack, nil, "main"),
   215  			)
   216  		})
   217  
   218  		It("hijacks the most recent one-off build as the specified user", func() {
   219  			hijack("-s", "some-step")
   220  		})
   221  	})
   222  
   223  	Context("when no containers are found", func() {
   224  		BeforeEach(func() {
   225  			didHijack := make(chan struct{})
   226  			hijacked = didHijack
   227  
   228  			atcServer.AppendHandlers(
   229  				ghttp.CombineHandlers(
   230  					ghttp.VerifyRequest("GET", "/api/v1/builds"),
   231  					ghttp.RespondWithJSONEncoded(200, []atc.Build{
   232  						{ID: 1, Name: "1", Status: "finished"},
   233  					}),
   234  				),
   235  				ghttp.CombineHandlers(
   236  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers", "build_id=1&step_name=some-step"),
   237  					ghttp.RespondWithJSONEncoded(200, []atc.Container{}),
   238  				),
   239  				hijackHandler("container-id-1", didHijack, nil, "main"),
   240  			)
   241  		})
   242  
   243  		It("return a friendly error message", func() {
   244  			flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "-s", "some-step")
   245  			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   246  			Expect(err).NotTo(HaveOccurred())
   247  
   248  			Eventually(sess).Should(gexec.Exit(1))
   249  
   250  			Expect(sess.Err).To(gbytes.Say("no containers matched your search parameters!\n\nthey may have expired if your build hasn't recently finished.\n"))
   251  		})
   252  
   253  		Context("when a url is passed", func() {
   254  			It("return a friendly error message", func() {
   255  				flyCmd := exec.Command(flyPath, "hijack", "-s", "some-step", "-u", fmt.Sprintf("%s/teams/%s", atcServer.URL(), teamName))
   256  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   257  				Expect(err).NotTo(HaveOccurred())
   258  
   259  				Eventually(sess).Should(gexec.Exit(1))
   260  
   261  				Expect(sess.Err).To(gbytes.Say("no containers matched your search parameters!\n\nthey may have expired if your build hasn't recently finished.\n"))
   262  			})
   263  
   264  			It("returns an error when target from url is not found", func() {
   265  				flyCmd := exec.Command(flyPath, "hijack", "-s", "some-step", "-u", fmt.Sprintf("%s/teams/%s", "http://faketarget.com", teamName))
   266  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   267  				Expect(err).NotTo(HaveOccurred())
   268  
   269  				Eventually(sess).Should(gexec.Exit(1))
   270  
   271  				Expect(sess.Err).To(gbytes.Say("no target matching url"))
   272  			})
   273  
   274  			It("returns an error when team name from url is not found", func() {
   275  				flyCmd := exec.Command(flyPath, "hijack", "-s", "some-step", "-u", fmt.Sprintf("%s/teams/%s/builds/0", atcServer.URL(), "faketeam"))
   276  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   277  				Expect(err).NotTo(HaveOccurred())
   278  
   279  				Eventually(sess).Should(gexec.Exit(1))
   280  
   281  				Expect(sess.Err).To(gbytes.Say("no target matching url"))
   282  			})
   283  		})
   284  	})
   285  
   286  	Context("when no containers are found", func() {
   287  		BeforeEach(func() {
   288  			didHijack := make(chan struct{})
   289  			hijacked = didHijack
   290  			atcServer.AppendHandlers(
   291  				ghttp.CombineHandlers(
   292  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers", "build_id=0"),
   293  					ghttp.RespondWithJSONEncoded(200, []atc.Container{}),
   294  				),
   295  			)
   296  		})
   297  
   298  		It("logs an error message and response status/body", func() {
   299  			flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "-b", "0")
   300  
   301  			stdin, err := flyCmd.StdinPipe()
   302  			Expect(err).NotTo(HaveOccurred())
   303  
   304  			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   305  			Expect(err).NotTo(HaveOccurred())
   306  
   307  			Eventually(sess.Err.Contents).Should(ContainSubstring("no containers matched your search parameters!\n\nthey may have expired if your build hasn't recently finished.\n"))
   308  
   309  			err = stdin.Close()
   310  			Expect(err).NotTo(HaveOccurred())
   311  
   312  			<-sess.Exited
   313  			Expect(sess.ExitCode()).To(Equal(1))
   314  		})
   315  	})
   316  
   317  	Context("when multiple step containers are found", func() {
   318  		var (
   319  			containerList       []atc.Container
   320  			didHijack           chan struct{}
   321  			expectedQueryParams []string
   322  		)
   323  
   324  		BeforeEach(func() {
   325  			didHijack = make(chan struct{})
   326  			hijacked = didHijack
   327  			containerList = []atc.Container{
   328  				{
   329  					ID:           "container-id-1",
   330  					WorkerName:   "worker-name-1",
   331  					PipelineName: "pipeline-name-1",
   332  					JobName:      "some-job",
   333  					BuildName:    "2",
   334  					BuildID:      12,
   335  					Type:         "get",
   336  					StepName:     "some-input",
   337  					Attempt:      "1.1.1",
   338  					User:         user,
   339  					State:        atc.ContainerStateCreated,
   340  				},
   341  				{
   342  					ID:           "container-id-2",
   343  					WorkerName:   "worker-name-2",
   344  					PipelineName: "pipeline-name-1",
   345  					JobName:      "some-job",
   346  					BuildName:    "2",
   347  					BuildID:      13,
   348  					Type:         "put",
   349  					StepName:     "some-output",
   350  					Attempt:      "1.1.2",
   351  					User:         user,
   352  					State:        atc.ContainerStateCreated,
   353  				},
   354  				{
   355  					ID:           "container-id-3",
   356  					WorkerName:   "worker-name-2",
   357  					PipelineName: "pipeline-name-2",
   358  					JobName:      "some-job",
   359  					BuildName:    "2",
   360  					BuildID:      13,
   361  					StepName:     "some-output",
   362  					Type:         "task",
   363  					Attempt:      "1",
   364  					User:         user,
   365  					State:        atc.ContainerStateCreated,
   366  				},
   367  				{
   368  					ID:           "container-id-4",
   369  					WorkerName:   "worker-name-2",
   370  					PipelineName: "pipeline-name-2",
   371  					ResourceName: "banana",
   372  					User:         user,
   373  					Type:         "check",
   374  					State:        atc.ContainerStateCreated,
   375  				},
   376  			}
   377  			expectedQueryParams = append([]string{}, "pipeline_name=pipeline-name-1", "job_name=some-job")
   378  		})
   379  
   380  		JustBeforeEach(func() {
   381  			atcServer.AppendHandlers(
   382  				ghttp.CombineHandlers(
   383  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers", strings.Join(expectedQueryParams, "&")),
   384  					ghttp.RespondWithJSONEncoded(200, containerList),
   385  				),
   386  				hijackHandler("container-id-2", didHijack, nil, "main"),
   387  			)
   388  		})
   389  
   390  		It("asks the user to select the container from a menu", func() {
   391  			flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "-j", "pipeline-name-1/some-job")
   392  
   393  			stdin, err := flyCmd.StdinPipe()
   394  			Expect(err).NotTo(HaveOccurred())
   395  
   396  			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   397  			Expect(err).NotTo(HaveOccurred())
   398  
   399  			Eventually(sess.Out).Should(gbytes.Say("1. resource: banana, type: check"))
   400  			Eventually(sess.Out).Should(gbytes.Say("2. build #2, step: some-input, type: get, attempt: 1.1.1"))
   401  			Eventually(sess.Out).Should(gbytes.Say("3. build #2, step: some-output, type: put, attempt: 1.1.2"))
   402  			Eventually(sess.Out).Should(gbytes.Say("4. build #2, step: some-output, type: task, attempt: 1"))
   403  			Eventually(sess.Out).Should(gbytes.Say("choose a container: "))
   404  
   405  			_, err = fmt.Fprintf(stdin, "3\n")
   406  			Expect(err).NotTo(HaveOccurred())
   407  
   408  			Eventually(hijacked).Should(BeClosed())
   409  
   410  			_, err = fmt.Fprintf(stdin, "some stdin")
   411  			Expect(err).NotTo(HaveOccurred())
   412  
   413  			Eventually(sess.Out).Should(gbytes.Say("some stdout"))
   414  			Eventually(sess.Err).Should(gbytes.Say("some stderr"))
   415  
   416  			err = stdin.Close()
   417  			Expect(err).NotTo(HaveOccurred())
   418  
   419  			<-sess.Exited
   420  			Expect(sess.ExitCode()).To(Equal(123))
   421  		})
   422  
   423  		Context("and no containers are in hijackable state", func() {
   424  			BeforeEach(func() {
   425  				containerList = []atc.Container{
   426  					{
   427  						ID:           "container-id-2",
   428  						WorkerName:   "worker-name-1",
   429  						PipelineName: "pipeline-name-1",
   430  						JobName:      "some-job",
   431  						BuildName:    "2",
   432  						BuildID:      12,
   433  						Type:         "get",
   434  						StepName:     "some-input",
   435  						Attempt:      "1.1.1",
   436  						User:         user,
   437  						State:        atc.ContainerStateCreating,
   438  					},
   439  				}
   440  			})
   441  
   442  			It("should show that no containers are hijackable", func() {
   443  				flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "-j", "pipeline-name-1/some-job")
   444  
   445  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   446  				Expect(err).NotTo(HaveOccurred())
   447  
   448  				<-sess.Exited
   449  				Expect(sess.ExitCode()).To(Equal(1))
   450  
   451  				Eventually(sess.Err).Should(gbytes.Say("no containers matched"))
   452  				close(didHijack)
   453  			})
   454  		})
   455  
   456  		Context("and some containers are in a non-hijackable state", func() {
   457  			BeforeEach(func() {
   458  				containerList = []atc.Container{
   459  					{
   460  						ID:           "container-id-1",
   461  						WorkerName:   "worker-name-1",
   462  						PipelineName: "pipeline-name-1",
   463  						JobName:      "some-job",
   464  						BuildName:    "2",
   465  						BuildID:      12,
   466  						Type:         "get",
   467  						StepName:     "some-input",
   468  						Attempt:      "1.1.1",
   469  						User:         user,
   470  						State:        atc.ContainerStateCreating,
   471  					},
   472  					{
   473  						ID:           "container-id-2",
   474  						WorkerName:   "worker-name-2",
   475  						PipelineName: "pipeline-name-1",
   476  						JobName:      "some-job",
   477  						BuildName:    "2",
   478  						BuildID:      13,
   479  						Type:         "put",
   480  						StepName:     "some-output",
   481  						Attempt:      "1.1.2",
   482  						User:         user,
   483  						State:        atc.ContainerStateCreated,
   484  					},
   485  					{
   486  						ID:           "container-id-3",
   487  						WorkerName:   "worker-name-2",
   488  						PipelineName: "pipeline-name-2",
   489  						JobName:      "some-job",
   490  						BuildName:    "2",
   491  						BuildID:      13,
   492  						StepName:     "some-output",
   493  						Type:         "task",
   494  						Attempt:      "1",
   495  						User:         user,
   496  						State:        atc.ContainerStateFailed,
   497  					},
   498  					{
   499  						ID:           "container-id-4",
   500  						WorkerName:   "worker-name-2",
   501  						PipelineName: "pipeline-name-2",
   502  						ResourceName: "banana",
   503  						User:         user,
   504  						Type:         "check",
   505  						State:        atc.ContainerStateDestroying,
   506  					},
   507  				}
   508  			})
   509  
   510  			It("should not display those containers in the list of results", func() {
   511  				flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "-j", "pipeline-name-1/some-job")
   512  
   513  				stdin, err := flyCmd.StdinPipe()
   514  				Expect(err).NotTo(HaveOccurred())
   515  
   516  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   517  				Expect(err).NotTo(HaveOccurred())
   518  
   519  				Eventually(sess.Out).Should(gbytes.Say("1. build #2, step: some-output, type: put, attempt: 1.1.2"))
   520  				Eventually(sess.Out).Should(gbytes.Say("2. build #2, step: some-output, type: task, attempt: 1"))
   521  				Eventually(sess.Out).Should(gbytes.Say("choose a container: "))
   522  
   523  				_, err = fmt.Fprintf(stdin, "1\n")
   524  				Expect(err).NotTo(HaveOccurred())
   525  
   526  				Eventually(hijacked).Should(BeClosed())
   527  
   528  				_, err = fmt.Fprintf(stdin, "some stdin")
   529  				Expect(err).NotTo(HaveOccurred())
   530  
   531  				Eventually(sess.Out).Should(gbytes.Say("some stdout"))
   532  				Eventually(sess.Err).Should(gbytes.Say("some stderr"))
   533  
   534  				err = stdin.Close()
   535  				Expect(err).NotTo(HaveOccurred())
   536  
   537  				<-sess.Exited
   538  				Expect(sess.ExitCode()).To(Equal(123))
   539  			})
   540  		})
   541  
   542  		Context("and only one container is in hijackable state", func() {
   543  			BeforeEach(func() {
   544  				containerList = []atc.Container{
   545  					{
   546  						ID:           "container-id-1",
   547  						WorkerName:   "worker-name-1",
   548  						PipelineName: "pipeline-name-1",
   549  						JobName:      "some-job",
   550  						BuildName:    "1",
   551  						BuildID:      12,
   552  						Type:         "get",
   553  						StepName:     "some-input",
   554  						Attempt:      "1.1.1",
   555  						User:         user,
   556  						State:        atc.ContainerStateDestroying,
   557  					},
   558  					{
   559  						ID:           "container-id-2",
   560  						WorkerName:   "worker-name-2",
   561  						PipelineName: "pipeline-name-1",
   562  						JobName:      "some-job",
   563  						BuildName:    "2",
   564  						BuildID:      13,
   565  						Type:         "put",
   566  						StepName:     "some-output",
   567  						Attempt:      "1.1.2",
   568  						User:         user,
   569  						State:        atc.ContainerStateCreated,
   570  					},
   571  				}
   572  			})
   573  
   574  			It("hijacks the hijackable container", func() {
   575  				flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "-j", "pipeline-name-1/some-job")
   576  
   577  				stdin, err := flyCmd.StdinPipe()
   578  				Expect(err).NotTo(HaveOccurred())
   579  
   580  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   581  				Expect(err).NotTo(HaveOccurred())
   582  
   583  				Eventually(hijacked).Should(BeClosed())
   584  
   585  				_, err = fmt.Fprintf(stdin, "some stdin")
   586  				Expect(err).NotTo(HaveOccurred())
   587  
   588  				Eventually(sess.Out).Should(gbytes.Say("some stdout"))
   589  				Eventually(sess.Err).Should(gbytes.Say("some stderr"))
   590  
   591  				err = stdin.Close()
   592  				Expect(err).NotTo(HaveOccurred())
   593  
   594  				<-sess.Exited
   595  				Expect(sess.ExitCode()).To(Equal(123))
   596  			})
   597  		})
   598  	})
   599  
   600  	Context("when hijack returns a single container", func() {
   601  		var (
   602  			containerArguments []string
   603  			stepType           string
   604  			stepName           string
   605  			buildID            int
   606  			hijackHandlerError []string
   607  			pipelineName       string
   608  			resourceName       string
   609  			jobName            string
   610  			buildName          string
   611  			attempt            string
   612  			hijackTeamName     string
   613  		)
   614  
   615  		BeforeEach(func() {
   616  			hijackHandlerError = nil
   617  			pipelineName = "a-pipeline"
   618  			jobName = ""
   619  			buildName = ""
   620  			buildID = 0
   621  			stepType = ""
   622  			stepName = ""
   623  			resourceName = ""
   624  			containerArguments = []string{}
   625  			attempt = ""
   626  			hijackTeamName = "main"
   627  		})
   628  
   629  		JustBeforeEach(func() {
   630  			didHijack := make(chan struct{})
   631  			hijacked = didHijack
   632  
   633  			atcServer.AppendHandlers(
   634  				ghttp.CombineHandlers(
   635  					ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName+"/containers", strings.Join(containerArguments, "&")),
   636  					ghttp.RespondWithJSONEncoded(200, []atc.Container{
   637  						{ID: "container-id-1", State: atc.ContainerStateCreated, WorkerName: "some-worker", PipelineName: pipelineName, JobName: jobName, BuildName: buildName, BuildID: buildID, Type: stepType, StepName: stepName, ResourceName: resourceName, Attempt: attempt, User: user},
   638  					}),
   639  				),
   640  				hijackHandler("container-id-1", didHijack, hijackHandlerError, hijackTeamName),
   641  			)
   642  		})
   643  
   644  		Context("when called with check container", func() {
   645  			BeforeEach(func() {
   646  				resourceName = "some-resource-name"
   647  				containerArguments = append(containerArguments, "type=check", "resource_name=some-resource-name", "pipeline_name=a-pipeline")
   648  			})
   649  
   650  			Context("when the team is 'main'", func() {
   651  				BeforeEach(func() {
   652  					hijackTeamName = "main"
   653  				})
   654  				Context("and with pipeline specified", func() {
   655  					It("can accept the check resources name and a pipeline", func() {
   656  						hijack("--check", "a-pipeline/some-resource-name")
   657  					})
   658  
   659  					It("hijacks the given check container by URL", func() {
   660  						hijack("--url", atcServer.URL()+"/teams/"+teamName+"/pipelines/a-pipeline/resources/some-resource-name")
   661  					})
   662  
   663  					Context("and with pipeline instance is specified", func() {
   664  						BeforeEach(func() {
   665  							containerArguments = append(containerArguments, "instance_vars=%7B%22branch%22%3A%22master%22%7D")
   666  						})
   667  
   668  						It("can accept the check resources name and a pipeline", func() {
   669  							hijack("--check", "a-pipeline/branch:master/some-resource-name")
   670  						})
   671  
   672  						It("hijacks the given check container by URL", func() {
   673  							hijack("--url", atcServer.URL()+"/teams/"+teamName+"/pipelines/a-pipeline/resources/some-resource-name"+"?instance_vars=%7B%22branch%22%3A%22master%22%7D")
   674  						})
   675  					})
   676  				})
   677  			})
   678  
   679  			Context("when the team is 'other'", func() {
   680  				BeforeEach(func() {
   681  					hijackTeamName = "other"
   682  
   683  					atcServer.AppendHandlers(
   684  						ghttp.CombineHandlers(
   685  							ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName),
   686  							ghttp.RespondWithJSONEncoded(http.StatusOK, atc.Team{Name: hijackTeamName})),
   687  					)
   688  				})
   689  
   690  				Context("and with pipeline specified", func() {
   691  					It("can accept the check resources name and a pipeline", func() {
   692  						hijack("--check", "a-pipeline/some-resource-name", "--team", hijackTeamName)
   693  					})
   694  				})
   695  
   696  				Context("and with url specified", func() {
   697  					It("hijacks the given check container by URL", func() {
   698  						hijack("--url", atcServer.URL()+"/teams/other/pipelines/a-pipeline/resources/some-resource-name", "--team", "other")
   699  					})
   700  				})
   701  			})
   702  		})
   703  
   704  		Context("when called with a specific build id", func() {
   705  			BeforeEach(func() {
   706  				containerArguments = append(containerArguments, "build_id=2", "step_name=some-step")
   707  				stepType = "task"
   708  				stepName = "some-step"
   709  				buildID = 2
   710  			})
   711  
   712  			Context("when the team is 'main'", func() {
   713  				BeforeEach(func() {
   714  					hijackTeamName = "main"
   715  				})
   716  				It("hijacks the most recent one-off build", func() {
   717  					hijack("-b", "2", "-s", "some-step")
   718  				})
   719  			})
   720  
   721  			Context("when the team is 'other'", func() {
   722  				BeforeEach(func() {
   723  					hijackTeamName = "other"
   724  
   725  					atcServer.AppendHandlers(
   726  						ghttp.CombineHandlers(
   727  							ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName),
   728  							ghttp.RespondWithJSONEncoded(http.StatusOK, atc.Team{Name: hijackTeamName})),
   729  					)
   730  				})
   731  
   732  				It("hijacks the most recent one-off build", func() {
   733  					hijack("-b", "2", "-s", "some-step", "--team", hijackTeamName)
   734  				})
   735  			})
   736  		})
   737  
   738  		Context("when called with a specific job", func() {
   739  			BeforeEach(func() {
   740  				containerArguments = append(containerArguments, "pipeline_name=some-pipeline", "job_name=some-job", "step_name=some-step")
   741  				jobName = "some-job"
   742  				buildName = "3"
   743  				buildID = 13
   744  				stepType = "task"
   745  				stepName = "some-step"
   746  			})
   747  
   748  			Context("hijacks the job's next build", func() {
   749  				Context("When the team is 'main'", func() {
   750  					BeforeEach(func() {
   751  						hijackTeamName = "main"
   752  					})
   753  
   754  					It("hijacks the job's next build with '<pipeline>/<job>'", func() {
   755  						hijack("--job", "some-pipeline/some-job", "--step", "some-step")
   756  					})
   757  
   758  					It("hijacks the job's next build when URL is specified", func() {
   759  						hijack("--url", atcServer.URL()+"/teams/"+teamName+"/pipelines/some-pipeline/jobs/some-job", "--step", "some-step")
   760  					})
   761  
   762  					Context("when pipeline instance is specified", func() {
   763  						BeforeEach(func() {
   764  							containerArguments = append(containerArguments, "instance_vars=%7B%22branch%22%3A%22master%22%7D")
   765  						})
   766  
   767  						It("hijacks the job's next build with '<pipeline>/<instance_vars>/<job>'", func() {
   768  							hijack("--job", "some-pipeline/branch:master/some-job", "--step", "some-step")
   769  						})
   770  
   771  						It("hijacks the job's next build when URL is specified", func() {
   772  							hijack("--url", atcServer.URL()+"/teams/"+teamName+"/pipelines/some-pipeline/jobs/some-job"+"?instance_vars=%7B%22branch%22%3A%22master%22%7D", "--step", "some-step")
   773  						})
   774  					})
   775  
   776  				})
   777  
   778  				Context("When the team is 'other'", func() {
   779  					BeforeEach(func() {
   780  						hijackTeamName = "other"
   781  
   782  						atcServer.AppendHandlers(
   783  							ghttp.CombineHandlers(
   784  								ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName),
   785  								ghttp.RespondWithJSONEncoded(http.StatusOK, atc.Team{Name: hijackTeamName})),
   786  						)
   787  					})
   788  					It("hijacks the job's next build with 'pipelineName/jobName'", func() {
   789  						hijack("--job", "some-pipeline/some-job", "--step", "some-step", "--team", hijackTeamName)
   790  					})
   791  
   792  					It("hijacks the job's next build when URL is specified", func() {
   793  						hijack("--url", atcServer.URL()+"/teams/other/pipelines/some-pipeline/jobs/some-job", "--step", "some-step", "--team", hijackTeamName)
   794  					})
   795  				})
   796  
   797  			})
   798  
   799  			Context("with a specific build of the job", func() {
   800  				BeforeEach(func() {
   801  					containerArguments = append(containerArguments, "build_name=3")
   802  				})
   803  
   804  				Context("When the team is 'main'", func() {
   805  					BeforeEach(func() {
   806  						hijackTeamName = "main"
   807  					})
   808  					It("hijacks the given build", func() {
   809  						hijack("--job", "some-pipeline/some-job", "--build", "3", "--step", "some-step")
   810  					})
   811  					It("hijacks the given build with URL", func() {
   812  						hijack("--url", atcServer.URL()+"/teams/main/pipelines/some-pipeline/jobs/some-job/builds/3", "--step", "some-step")
   813  					})
   814  				})
   815  
   816  				Context("When the team is 'other'", func() {
   817  					BeforeEach(func() {
   818  						hijackTeamName = "other"
   819  
   820  						atcServer.AppendHandlers(
   821  							ghttp.CombineHandlers(
   822  								ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName),
   823  								ghttp.RespondWithJSONEncoded(http.StatusOK, atc.Team{Name: hijackTeamName})),
   824  						)
   825  					})
   826  					It("hijacks the given build", func() {
   827  						hijack("--job", "some-pipeline/some-job", "--build", "3", "--step", "some-step", "--team", hijackTeamName)
   828  					})
   829  					It("hijacks the given build with URL", func() {
   830  						hijack("--url", atcServer.URL()+"/teams/other/pipelines/some-pipeline/jobs/some-job/builds/3", "--step", "some-step", "--team", hijackTeamName)
   831  					})
   832  				})
   833  
   834  			})
   835  		})
   836  
   837  		Context("when called with a specific attempt number", func() {
   838  			BeforeEach(func() {
   839  				containerArguments = append(containerArguments, "pipeline_name=some-pipeline", "job_name=some-job", "step_name=some-step", "attempt=2.4")
   840  				jobName = "some-job"
   841  				buildName = "3"
   842  				buildID = 13
   843  				stepType = "task"
   844  				stepName = "some-step"
   845  				attempt = "2.4"
   846  			})
   847  
   848  			Context("When the team is 'main'", func() {
   849  				BeforeEach(func() {
   850  					hijackTeamName = "main"
   851  				})
   852  				It("hijacks the job's next build", func() {
   853  					hijack("--job", "some-pipeline/some-job", "--step", "some-step", "--attempt", "2.4")
   854  				})
   855  			})
   856  
   857  			Context("When the team is 'other'", func() {
   858  				BeforeEach(func() {
   859  					hijackTeamName = "other"
   860  
   861  					atcServer.AppendHandlers(
   862  						ghttp.CombineHandlers(
   863  							ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName),
   864  							ghttp.RespondWithJSONEncoded(http.StatusOK, atc.Team{Name: hijackTeamName})),
   865  					)
   866  				})
   867  				It("hijacks the job's next build", func() {
   868  					hijack("--job", "some-pipeline/some-job", "--step", "some-step", "--attempt", "2.4", "--team", hijackTeamName)
   869  				})
   870  			})
   871  		})
   872  
   873  		Context("when called with a step type", func() {
   874  			BeforeEach(func() {
   875  				containerArguments = append(containerArguments, "pipeline_name=some-pipeline", "job_name=some-job", "step_name=some-step", "type=put")
   876  				jobName = "some-job"
   877  				buildName = "3"
   878  				buildID = 13
   879  				stepType = "put"
   880  				stepName = "some-step"
   881  				attempt = ""
   882  			})
   883  
   884  			It("hijacks the job's next build", func() {
   885  				hijack("--job", "some-pipeline/some-job", "--step", "some-step", "--step-type", "put")
   886  			})
   887  		})
   888  
   889  		Context("when called with a specific path and args", func() {
   890  			BeforeEach(func() {
   891  				path = "sh"
   892  				args = []string{"echo hello"}
   893  
   894  				containerArguments = append(containerArguments, "build_id=2", "step_name=some-step")
   895  				stepType = "task"
   896  				stepName = "some-step"
   897  				buildID = 2
   898  			})
   899  
   900  			It("hijacks and runs the provided path with args", func() {
   901  				hijack("-b", "2", "-s", "some-step", "sh", "echo hello")
   902  			})
   903  		})
   904  
   905  		Context("when hijacking yields an error", func() {
   906  			BeforeEach(func() {
   907  				resourceName = "some-resource-name"
   908  				containerArguments = append(containerArguments, "type=check", "resource_name=some-resource-name", "pipeline_name=a-pipeline")
   909  				hijackHandlerError = []string{"something went wrong"}
   910  			})
   911  
   912  			It("prints it to stderr and exits 255", func() {
   913  				flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "--check", "a-pipeline/some-resource-name")
   914  
   915  				stdin, err := flyCmd.StdinPipe()
   916  				Expect(err).NotTo(HaveOccurred())
   917  
   918  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
   919  				Expect(err).NotTo(HaveOccurred())
   920  
   921  				Eventually(hijacked).Should(BeClosed())
   922  
   923  				_, err = fmt.Fprintf(stdin, "some stdin")
   924  				Expect(err).NotTo(HaveOccurred())
   925  
   926  				Eventually(sess.Err.Contents).Should(ContainSubstring(ansi.Color("something went wrong", "red+b") + "\n"))
   927  
   928  				err = stdin.Close()
   929  				Expect(err).NotTo(HaveOccurred())
   930  
   931  				<-sess.Exited
   932  				Expect(sess.ExitCode()).To(Equal(255))
   933  			})
   934  		})
   935  	})
   936  
   937  	Context("when hijacking a specific container", func() {
   938  		var (
   939  			hijackHandlerError []string
   940  			statusCode         int
   941  			id                 string
   942  			hijackTeamName     string
   943  		)
   944  
   945  		BeforeEach(func() {
   946  			hijackHandlerError = nil
   947  			statusCode = 0
   948  			id = ""
   949  			hijackTeamName = "main"
   950  
   951  		})
   952  
   953  		JustBeforeEach(func() {
   954  			atcServer.AppendHandlers(
   955  				ghttp.CombineHandlers(
   956  					ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName+"/containers/container-id"),
   957  					ghttp.RespondWithJSONEncoded(statusCode, atc.Container{
   958  						ID:   id,
   959  						User: user,
   960  					}),
   961  				),
   962  			)
   963  		})
   964  
   965  		Context("when container exists", func() {
   966  			BeforeEach(func() {
   967  				statusCode = 200
   968  				id = "container-id"
   969  			})
   970  
   971  			Context("when hijack returns no error", func() {
   972  				JustBeforeEach(func() {
   973  					didHijack := make(chan struct{})
   974  					hijacked = didHijack
   975  					atcServer.AppendHandlers(
   976  						hijackHandler("container-id", didHijack, hijackHandlerError, hijackTeamName),
   977  					)
   978  				})
   979  				Context("when the team is 'main'", func() {
   980  					BeforeEach(func() {
   981  						hijackTeamName = "main"
   982  					})
   983  					It("should hijack container with associated handle", func() {
   984  						hijack("--handle", "container-id")
   985  					})
   986  				})
   987  
   988  				Context("when the team is 'other'", func() {
   989  					BeforeEach(func() {
   990  						hijackTeamName = "other"
   991  
   992  						atcServer.AppendHandlers(
   993  							ghttp.CombineHandlers(
   994  								ghttp.VerifyRequest("GET", "/api/v1/teams/"+hijackTeamName),
   995  								ghttp.RespondWithJSONEncoded(http.StatusOK, atc.Team{Name: hijackTeamName})),
   996  						)
   997  					})
   998  					It("should hijack container with associated handle to 'other' team", func() {
   999  						hijack("--handle", "container-id", "--team", hijackTeamName)
  1000  					})
  1001  				})
  1002  			})
  1003  
  1004  			Context("when hijack returns error", func() {
  1005  				JustBeforeEach(func() {
  1006  					atcServer.AppendHandlers(
  1007  						ghttp.CombineHandlers(
  1008  							ghttp.VerifyRequest("GET", fmt.Sprintf("/api/v1/teams/main/containers/%s/hijack", id)),
  1009  							ghttp.RespondWithJSONEncoded(403, nil),
  1010  						),
  1011  					)
  1012  				})
  1013  
  1014  				It("should print out response status and error", func() {
  1015  					flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "--handle", "container-id")
  1016  					sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
  1017  					Expect(err).NotTo(HaveOccurred())
  1018  
  1019  					Eventually(sess).Should(gexec.Exit(1))
  1020  
  1021  					Expect(sess.Err).To(gbytes.Say("error: 403 Forbidden websocket: bad handshake"))
  1022  				})
  1023  			})
  1024  		})
  1025  
  1026  		Context("when container does not exist", func() {
  1027  			BeforeEach(func() {
  1028  				statusCode = 404
  1029  			})
  1030  
  1031  			JustBeforeEach(func() {
  1032  				didHijack := make(chan struct{})
  1033  				hijacked = didHijack
  1034  				atcServer.AppendHandlers(
  1035  					hijackHandler("container-id", didHijack, hijackHandlerError, hijackTeamName),
  1036  				)
  1037  			})
  1038  
  1039  			It("should return an appropriate error message", func() {
  1040  				flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "--handle", "container-id")
  1041  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
  1042  				Expect(err).NotTo(HaveOccurred())
  1043  
  1044  				Eventually(sess).Should(gexec.Exit(1))
  1045  
  1046  				Expect(sess.Err).To(gbytes.Say("no containers matched the given handle id!\n\nthey may have expired if your build hasn't recently finished.\n"))
  1047  			})
  1048  		})
  1049  	})
  1050  
  1051  	Context("when passing a URL that doesn't match the target", func() {
  1052  		It("errors out when wrong team is specified", func() {
  1053  			flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "--url", atcServer.URL()+"/teams/wrongteam/pipelines/a-pipeline/resources/some-resource-name")
  1054  
  1055  			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
  1056  			Expect(err).NotTo(HaveOccurred())
  1057  
  1058  			Eventually(sess.Err.Contents).Should(ContainSubstring("Team in URL doesn't match the current team of the target"))
  1059  
  1060  			<-sess.Exited
  1061  			Expect(sess.ExitCode()).ToNot(Equal(0))
  1062  		})
  1063  
  1064  		It("errors out when wrong URL is specified", func() {
  1065  			flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "--url", "http://wrong.example.com/teams/"+teamName+"/pipelines/a-pipeline/resources/some-resource-name")
  1066  
  1067  			sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
  1068  			Expect(err).NotTo(HaveOccurred())
  1069  
  1070  			Eventually(sess.Err.Contents).Should(ContainSubstring("URL doesn't match that of target"))
  1071  
  1072  			<-sess.Exited
  1073  			Expect(sess.ExitCode()).ToNot(Equal(0))
  1074  		})
  1075  	})
  1076  
  1077  	Context("when hijacking yields an executable not found error", func() {
  1078  		var hijacked2 <-chan struct{}
  1079  		JustBeforeEach(func() {
  1080  			didHijack := make(chan struct{})
  1081  			hijacked = didHijack
  1082  			didHijack2 := make(chan struct{})
  1083  			hijacked2 = didHijack2
  1084  			atcServer.AppendHandlers(
  1085  				ghttp.CombineHandlers(
  1086  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers", "type=check&resource_name=some-resource-name&pipeline_name=a-pipeline"),
  1087  					ghttp.RespondWithJSONEncoded(200, []atc.Container{
  1088  						{ID: "container-id-1", State: atc.ContainerStateCreated, WorkerName: "some-worker", PipelineName: "a-pipeline", JobName: "", BuildName: "", BuildID: 0, Type: "", StepName: "", ResourceName: "some-resource-name", Attempt: "", User: user},
  1089  					}),
  1090  				),
  1091  				ghttp.CombineHandlers(
  1092  					ghttp.VerifyRequest("GET", "/api/v1/teams/main/containers/container-id-1/hijack"),
  1093  					func(w http.ResponseWriter, r *http.Request) {
  1094  						defer GinkgoRecover()
  1095  
  1096  						conn, err := upgrader.Upgrade(w, r, nil)
  1097  						Expect(err).NotTo(HaveOccurred())
  1098  
  1099  						defer conn.Close()
  1100  
  1101  						close(didHijack)
  1102  
  1103  						var processSpec atc.HijackProcessSpec
  1104  						err = conn.ReadJSON(&processSpec)
  1105  						Expect(err).NotTo(HaveOccurred())
  1106  
  1107  						Expect(processSpec.User).To(Equal(user))
  1108  						Expect(processSpec.Dir).To(Equal(workingDirectory))
  1109  						Expect(processSpec.Path).To(Equal("bash"))
  1110  						Expect(processSpec.Args).To(Equal(args))
  1111  
  1112  						err = conn.WriteJSON(atc.HijackOutput{
  1113  							ExecutableNotFound: true,
  1114  						})
  1115  						Expect(err).NotTo(HaveOccurred())
  1116  
  1117  						err = conn.WriteJSON(atc.HijackOutput{
  1118  							Error: "executable not found",
  1119  						})
  1120  						Expect(err).NotTo(HaveOccurred())
  1121  					},
  1122  				),
  1123  				hijackHandler("container-id-1", didHijack2, nil, "main"),
  1124  			)
  1125  		})
  1126  
  1127  		Context("when a path was not specified", func() {
  1128  			BeforeEach(func() {
  1129  				path = "sh"
  1130  			})
  1131  			It("tries \"bash\" then \"sh\"", func() {
  1132  				os.Stdout.WriteString("\n")
  1133  				flyCmd := exec.Command(flyPath, "-t", targetName, "hijack", "--check", "a-pipeline/some-resource-name")
  1134  
  1135  				stdin, err := flyCmd.StdinPipe()
  1136  				Expect(err).NotTo(HaveOccurred())
  1137  
  1138  				sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
  1139  				Expect(err).NotTo(HaveOccurred())
  1140  
  1141  				Eventually(hijacked).Should(BeClosed())
  1142  
  1143  				Eventually(sess.Err.Contents).Should(ContainSubstring(ansi.Color("executable not found", "red+b") + "\n"))
  1144  				Eventually(sess.Err.Contents).Should(ContainSubstring("Couldn't find \"bash\" on container, retrying with \"sh\""))
  1145  
  1146  				Eventually(hijacked2).Should(BeClosed())
  1147  
  1148  				_, err = fmt.Fprintf(stdin, "some stdin")
  1149  				Expect(err).NotTo(HaveOccurred())
  1150  
  1151  				Eventually(sess.Out).Should(gbytes.Say("some stdout"))
  1152  				Eventually(sess.Err).Should(gbytes.Say("some stderr"))
  1153  
  1154  				err = stdin.Close()
  1155  				Expect(err).NotTo(HaveOccurred())
  1156  
  1157  				<-sess.Exited
  1158  				Expect(sess.ExitCode()).To(Equal(123))
  1159  			})
  1160  		})
  1161  	})
  1162  })