1package commands
2
3import (
4	"fmt"
5	"io"
6	"net/url"
7	"os"
8	"sort"
9	"strconv"
10	"strings"
11
12	"github.com/concourse/concourse/atc"
13	"github.com/concourse/concourse/fly/commands/internal/displayhelpers"
14	"github.com/concourse/concourse/fly/commands/internal/flaghelpers"
15	"github.com/concourse/concourse/fly/commands/internal/hijacker"
16	"github.com/concourse/concourse/fly/commands/internal/hijackhelpers"
17	"github.com/concourse/concourse/fly/pty"
18	"github.com/concourse/concourse/fly/rc"
19	"github.com/concourse/concourse/go-concourse/concourse"
20	"github.com/tedsuo/rata"
21	"github.com/vito/go-interact/interact"
22)
23
24type HijackCommand struct {
25	Job            flaghelpers.JobFlag      `short:"j" long:"job"   value-name:"PIPELINE/JOB"   description:"Name of a job to hijack"`
26	Handle         string                   `          long:"handle"                            description:"Handle id of a job to hijack"`
27	Check          flaghelpers.ResourceFlag `short:"c" long:"check" value-name:"PIPELINE/CHECK" description:"Name of a resource's checking container to hijack"`
28	Url            string                   `short:"u" long:"url"                               description:"URL for the build, job, or check container to hijack"`
29	Build          string                   `short:"b" long:"build"                             description:"Build number within the job, or global build ID"`
30	StepName       string                   `short:"s" long:"step"                              description:"Name of step to hijack (e.g. build, unit, resource name)"`
31	StepType       string                   `          long:"step-type"                         description:"Type of step to hijack (e.g. get, put, task)"`
32	Attempt        string                   `short:"a" long:"attempt" value-name:"N[,N,...]"    description:"Attempt number of step to hijack."`
33	PositionalArgs struct {
34		Command []string `positional-arg-name:"command" description:"The command to run in the container (default: bash)"`
35	} `positional-args:"yes"`
36	Team string `long:"team" description:"Name of the team to which the container belongs, if different from the target default"`
37}
38
39func (command *HijackCommand) Execute([]string) error {
40	var (
41		chosenContainer atc.Container
42		err             error
43		name            rc.TargetName
44		target          rc.Target
45		team            concourse.Team
46	)
47	if Fly.Target == "" && command.Url != "" {
48		u, err := url.Parse(command.Url)
49		if err != nil {
50			return err
51		}
52		urlMap := parseUrlPath(u.Path)
53		target, name, err = rc.LoadTargetFromURL(fmt.Sprintf("%s://%s", u.Scheme, u.Host), urlMap["teams"], Fly.Verbose)
54		if err != nil {
55			return err
56		}
57		Fly.Target = name
58	} else {
59		target, err = rc.LoadTarget(Fly.Target, Fly.Verbose)
60		if err != nil {
61			return err
62		}
63	}
64
65	err = target.Validate()
66	if err != nil {
67		return err
68	}
69
70	if command.Team != "" {
71		team, err = target.FindTeam(command.Team)
72		if err != nil {
73			return err
74		}
75	} else {
76		team = target.Team()
77	}
78
79	if command.Handle != "" {
80		chosenContainer, err = team.GetContainer(command.Handle)
81		if err != nil {
82			displayhelpers.Failf("no containers matched the given handle id!\n\nthey may have expired if your build hasn't recently finished.")
83		}
84
85	} else {
86		fingerprint, err := command.getContainerFingerprint(target, team)
87		if err != nil {
88			return err
89		}
90
91		containers, err := command.getContainerIDs(target, fingerprint, team)
92		if err != nil {
93			return err
94		}
95
96		hijackableContainers := make([]atc.Container, 0)
97
98		for _, container := range containers {
99			if container.State == atc.ContainerStateCreated || container.State == atc.ContainerStateFailed {
100				hijackableContainers = append(hijackableContainers, container)
101			}
102		}
103
104		if len(hijackableContainers) == 0 {
105			displayhelpers.Failf("no containers matched your search parameters!\n\nthey may have expired if your build hasn't recently finished.")
106		} else if len(hijackableContainers) > 1 {
107			var choices []interact.Choice
108			for _, container := range hijackableContainers {
109				var infos []string
110
111				if container.BuildID != 0 {
112					if container.JobName != "" {
113						infos = append(infos, fmt.Sprintf("build #%s", container.BuildName))
114					} else {
115						infos = append(infos, fmt.Sprintf("build id: %d", container.BuildID))
116					}
117				}
118
119				if container.StepName != "" {
120					infos = append(infos, fmt.Sprintf("step: %s", container.StepName))
121				}
122
123				if container.ResourceName != "" {
124					infos = append(infos, fmt.Sprintf("resource: %s", container.ResourceName))
125				}
126
127				infos = append(infos, fmt.Sprintf("type: %s", container.Type))
128
129				if container.Type == "check" {
130					infos = append(infos, fmt.Sprintf("expires in: %s", container.ExpiresIn))
131				}
132
133				if container.Attempt != "" {
134					infos = append(infos, fmt.Sprintf("attempt: %s", container.Attempt))
135				}
136
137				choices = append(choices, interact.Choice{
138					Display: strings.Join(infos, ", "),
139					Value:   container,
140				})
141			}
142
143			err = interact.NewInteraction("choose a container", choices...).Resolve(&chosenContainer)
144			if err == io.EOF {
145				return nil
146			}
147
148			if err != nil {
149				return err
150			}
151		} else {
152			chosenContainer = hijackableContainers[0]
153		}
154	}
155
156	privileged := true
157
158	reqGenerator := rata.NewRequestGenerator(target.URL(), atc.Routes)
159
160	var ttySpec *atc.HijackTTYSpec
161	rows, cols, err := pty.Getsize(os.Stdout)
162	if err == nil {
163		ttySpec = &atc.HijackTTYSpec{
164			WindowSize: atc.HijackWindowSize{
165				Columns: cols,
166				Rows:    rows,
167			},
168		}
169	}
170
171	path, args := remoteCommand(command.PositionalArgs.Command)
172
173	spec := atc.HijackProcessSpec{
174		Path: path,
175		Args: args,
176		Env:  []string{"TERM=" + os.Getenv("TERM")},
177		User: chosenContainer.User,
178		Dir:  chosenContainer.WorkingDirectory,
179
180		Privileged: privileged,
181		TTY:        ttySpec,
182	}
183
184	result, err := func() (int, error) { // so the term.Restore() can run before the os.Exit()
185		var in io.Reader
186
187		if pty.IsTerminal() {
188			term, err := pty.OpenRawTerm()
189			if err != nil {
190				return -1, err
191			}
192
193			defer func() {
194				_ = term.Restore()
195			}()
196
197			in = term
198		} else {
199			in = os.Stdin
200		}
201
202		io := hijacker.ProcessIO{
203			In:  in,
204			Out: os.Stdout,
205			Err: os.Stderr,
206		}
207
208		h := hijacker.New(target.TLSConfig(), reqGenerator, target.Token())
209
210		return h.Hijack(team.Name(), chosenContainer.ID, spec, io)
211	}()
212
213	if err != nil {
214		return err
215	}
216
217	os.Exit(result)
218
219	return nil
220}
221
222func parseUrlPath(urlPath string) map[string]string {
223	pathWithoutFirstSlash := strings.Replace(urlPath, "/", "", 1)
224	urlComponents := strings.Split(pathWithoutFirstSlash, "/")
225	urlMap := make(map[string]string)
226
227	for i := 0; i < len(urlComponents)/2; i++ {
228		keyIndex := i * 2
229		valueIndex := keyIndex + 1
230		urlMap[urlComponents[keyIndex]] = urlComponents[valueIndex]
231	}
232
233	return urlMap
234}
235
236func (command *HijackCommand) getContainerFingerprintFromUrl(target rc.Target, urlParam string, team concourse.Team) (*containerFingerprint, error) {
237	u, err := url.Parse(urlParam)
238	if err != nil {
239		return nil, err
240	}
241
242	urlMap := parseUrlPath(u.Path)
243
244	parsedTargetUrl := url.URL{
245		Scheme: u.Scheme,
246		Host:   u.Host,
247	}
248
249	host := parsedTargetUrl.String()
250	if host != target.URL() {
251		err = fmt.Errorf("URL doesn't match that of target")
252		return nil, err
253	}
254
255	teamFromUrl := urlMap["teams"]
256
257	if teamFromUrl != team.Name() {
258		err = fmt.Errorf("Team in URL doesn't match the current team of the target")
259		return nil, err
260	}
261
262	fingerprint := &containerFingerprint{
263		pipelineName:  urlMap["pipelines"],
264		jobName:       urlMap["jobs"],
265		buildNameOrID: urlMap["builds"],
266		checkName:     urlMap["resources"],
267	}
268
269	return fingerprint, nil
270}
271
272func (command *HijackCommand) getContainerFingerprint(target rc.Target, team concourse.Team) (*containerFingerprint, error) {
273	var err error
274	fingerprint := &containerFingerprint{}
275
276	if command.Url != "" {
277		fingerprint, err = command.getContainerFingerprintFromUrl(target, command.Url, team)
278		if err != nil {
279			return nil, err
280		}
281	}
282
283	pipelineName := command.Check.PipelineName
284	if command.Job.PipelineName != "" {
285		pipelineName = command.Job.PipelineName
286	}
287
288	for _, field := range []struct {
289		fp  *string
290		cmd string
291	}{
292		{fp: &fingerprint.pipelineName, cmd: pipelineName},
293		{fp: &fingerprint.buildNameOrID, cmd: command.Build},
294		{fp: &fingerprint.stepName, cmd: command.StepName},
295		{fp: &fingerprint.stepType, cmd: command.StepType},
296		{fp: &fingerprint.jobName, cmd: command.Job.JobName},
297		{fp: &fingerprint.checkName, cmd: command.Check.ResourceName},
298		{fp: &fingerprint.attempt, cmd: command.Attempt},
299	} {
300		if field.cmd != "" {
301			*field.fp = field.cmd
302		}
303	}
304
305	return fingerprint, nil
306}
307
308func (command *HijackCommand) getContainerIDs(target rc.Target, fingerprint *containerFingerprint, team concourse.Team) ([]atc.Container, error) {
309	reqValues, err := locateContainer(target.Client(), fingerprint)
310	if err != nil {
311		return nil, err
312	}
313
314	containers, err := team.ListContainers(reqValues)
315	if err != nil {
316		return nil, err
317	}
318	sort.Sort(hijackhelpers.ContainerSorter(containers))
319
320	return containers, nil
321}
322
323func remoteCommand(argv []string) (string, []string) {
324	var path string
325	var args []string
326
327	switch len(argv) {
328	case 0:
329		path = "bash"
330	case 1:
331		path = argv[0]
332	default:
333		path = argv[0]
334		args = argv[1:]
335	}
336
337	return path, args
338}
339
340type containerLocator interface {
341	locate(*containerFingerprint) (map[string]string, error)
342}
343
344type stepContainerLocator struct {
345	client concourse.Client
346}
347
348func (locator stepContainerLocator) locate(fingerprint *containerFingerprint) (map[string]string, error) {
349	reqValues := map[string]string{}
350
351	if fingerprint.stepType != "" {
352		reqValues["type"] = fingerprint.stepType
353	}
354
355	if fingerprint.stepName != "" {
356		reqValues["step_name"] = fingerprint.stepName
357	}
358
359	if fingerprint.attempt != "" {
360		reqValues["attempt"] = fingerprint.attempt
361	}
362
363	if fingerprint.jobName != "" {
364		reqValues["pipeline_name"] = fingerprint.pipelineName
365		reqValues["job_name"] = fingerprint.jobName
366		if fingerprint.buildNameOrID != "" {
367			reqValues["build_name"] = fingerprint.buildNameOrID
368		}
369	} else if fingerprint.buildNameOrID != "" {
370		reqValues["build_id"] = fingerprint.buildNameOrID
371	} else {
372		build, err := GetBuild(locator.client, nil, "", "", "")
373		if err != nil {
374			return reqValues, err
375		}
376		reqValues["build_id"] = strconv.Itoa(build.ID)
377	}
378
379	return reqValues, nil
380}
381
382type checkContainerLocator struct{}
383
384func (locator checkContainerLocator) locate(fingerprint *containerFingerprint) (map[string]string, error) {
385	reqValues := map[string]string{}
386
387	reqValues["type"] = "check"
388	if fingerprint.checkName != "" {
389		reqValues["resource_name"] = fingerprint.checkName
390	}
391	if fingerprint.pipelineName != "" {
392		reqValues["pipeline_name"] = fingerprint.pipelineName
393	}
394
395	return reqValues, nil
396}
397
398type containerFingerprint struct {
399	pipelineName  string
400	jobName       string
401	buildNameOrID string
402
403	stepName string
404	stepType string
405
406	checkName string
407	attempt   string
408}
409
410func locateContainer(client concourse.Client, fingerprint *containerFingerprint) (map[string]string, error) {
411	var locator containerLocator
412
413	if fingerprint.checkName == "" {
414		locator = stepContainerLocator{
415			client: client,
416		}
417	} else {
418		locator = checkContainerLocator{}
419	}
420
421	return locator.locate(fingerprint)
422}
423