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

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