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 }