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