github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/kubetest/util.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "fmt" 22 "io" 23 "log" 24 "net/http" 25 "net/url" 26 "os" 27 "os/exec" 28 "os/signal" 29 "path/filepath" 30 "strings" 31 "sync" 32 "syscall" 33 "time" 34 ) 35 36 var ( 37 termLock = new(sync.RWMutex) 38 terminated = false 39 intLock = new(sync.RWMutex) 40 interrupted = false 41 ) 42 43 func isTerminated() bool { 44 termLock.RLock() 45 t := terminated 46 termLock.RUnlock() 47 return t 48 } 49 50 func isInterrupted() bool { 51 intLock.RLock() 52 i := interrupted 53 intLock.RUnlock() 54 return i 55 } 56 57 var httpTransport *http.Transport 58 59 func init() { 60 httpTransport = new(http.Transport) 61 httpTransport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) 62 } 63 64 // Returns $GOPATH/src/k8s.io/... 65 func k8s(parts ...string) string { 66 p := []string{os.Getenv("GOPATH"), "src", "k8s.io"} 67 for _, a := range parts { 68 p = append(p, a) 69 } 70 return filepath.Join(p...) 71 } 72 73 // append(errs, err) if err != nil 74 func appendError(errs []error, err error) []error { 75 if err != nil { 76 return append(errs, err) 77 } 78 return errs 79 } 80 81 // Returns $HOME/part/part/part 82 func home(parts ...string) string { 83 p := []string{os.Getenv("HOME")} 84 for _, a := range parts { 85 p = append(p, a) 86 } 87 return filepath.Join(p...) 88 } 89 90 // export PATH=path:$PATH 91 func insertPath(path string) error { 92 return os.Setenv("PATH", fmt.Sprintf("%v:%v", path, os.Getenv("PATH"))) 93 } 94 95 // Essentially curl url | writer 96 func httpRead(url string, writer io.Writer) error { 97 log.Printf("curl %s", url) 98 c := &http.Client{Transport: httpTransport} 99 r, err := c.Get(url) 100 if err != nil { 101 return err 102 } 103 defer r.Body.Close() 104 if r.StatusCode >= 400 { 105 return fmt.Errorf("%v returned %d", url, r.StatusCode) 106 } 107 _, err = io.Copy(writer, r.Body) 108 if err != nil { 109 return err 110 } 111 return nil 112 } 113 114 // return f(), adding junit xml testcase result for name 115 func xmlWrap(name string, f func() error) error { 116 alreadyInterrupted := isInterrupted() 117 start := time.Now() 118 err := f() 119 duration := time.Since(start) 120 c := testCase{ 121 Name: name, 122 ClassName: "e2e.go", 123 Time: duration.Seconds(), 124 } 125 if err == nil && !alreadyInterrupted && isInterrupted() { 126 err = fmt.Errorf("kubetest interrupted during step %s", name) 127 } 128 if err != nil { 129 if !alreadyInterrupted { 130 c.Failure = err.Error() 131 } else { 132 c.Skipped = err.Error() 133 } 134 suite.Failures++ 135 } 136 137 suite.Cases = append(suite.Cases, c) 138 suite.Tests++ 139 return err 140 } 141 142 // return cmd.Wait() and/or timing out. 143 func finishRunning(cmd *exec.Cmd) error { 144 stepName := strings.Join(cmd.Args, " ") 145 if isTerminated() { 146 return fmt.Errorf("skipped %s (kubetest is terminated)", stepName) 147 } 148 if cmd.Stdout == nil && verbose { 149 cmd.Stdout = os.Stdout 150 } 151 if cmd.Stderr == nil && verbose { 152 cmd.Stderr = os.Stderr 153 } 154 log.Printf("Running: %v", stepName) 155 defer func(start time.Time) { 156 log.Printf("Step '%s' finished in %s", stepName, time.Since(start)) 157 }(time.Now()) 158 159 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 160 if err := cmd.Start(); err != nil { 161 return fmt.Errorf("error starting %v: %v", stepName, err) 162 } 163 164 finished := make(chan error) 165 166 sigChannel := make(chan os.Signal, 1) 167 signal.Notify(sigChannel, os.Interrupt) 168 169 go func() { 170 finished <- cmd.Wait() 171 }() 172 173 for { 174 select { 175 case <-sigChannel: 176 log.Printf("Killing %v(%v) after receiving signal", stepName, -cmd.Process.Pid) 177 if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil { 178 log.Printf("Failed to kill %v: %v", stepName, err) 179 } 180 case <-terminate.C: 181 termLock.Lock() 182 terminated = true 183 termLock.Unlock() 184 terminate.Reset(time.Duration(0)) // Kill subsequent processes immediately. 185 if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil { 186 log.Printf("Failed to kill %v: %v", stepName, err) 187 } 188 if err := cmd.Process.Kill(); err != nil { 189 log.Printf("Failed to terminate %s (terminated 15m after interrupt): %v", stepName, err) 190 } 191 case <-interrupt.C: 192 intLock.Lock() 193 interrupted = true 194 intLock.Unlock() 195 log.Printf("Interrupt after %s timeout during %s. Will terminate in another 15m", timeout, stepName) 196 terminate.Reset(15 * time.Minute) 197 if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGINT); err != nil { 198 log.Printf("Failed to interrupt %s. Will terminate immediately: %v", stepName, err) 199 syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM) 200 cmd.Process.Kill() 201 } 202 case err := <-finished: 203 if err != nil { 204 var suffix string 205 if isTerminated() { 206 suffix = " (terminated)" 207 } else if isInterrupted() { 208 suffix = " (interrupted)" 209 } 210 return fmt.Errorf("error during %s%s: %v", stepName, suffix, err) 211 } 212 return err 213 } 214 } 215 } 216 217 type cmdExecResult struct { 218 stepName string 219 output string 220 execTime time.Duration 221 err error 222 } 223 224 // execute a given command and send output and error via channel 225 func executeParallelCommand(cmd *exec.Cmd, resChan chan cmdExecResult, termChan, intChan chan struct{}) { 226 stepName := strings.Join(cmd.Args, " ") 227 stdout := bytes.Buffer{} 228 cmd.Stdout = &stdout 229 cmd.Stderr = &stdout 230 231 start := time.Now() 232 log.Printf("Running: %v in parallel", stepName) 233 234 if isTerminated() { 235 resChan <- cmdExecResult{stepName: stepName, output: stdout.String(), execTime: time.Since(start), err: fmt.Errorf("skipped %s (kubetest is terminated)", stepName)} 236 return 237 } 238 239 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 240 if err := cmd.Start(); err != nil { 241 resChan <- cmdExecResult{stepName: stepName, output: stdout.String(), execTime: time.Since(start), err: fmt.Errorf("error starting %v: %v", stepName, err)} 242 return 243 } 244 245 finished := make(chan error) 246 go func() { 247 finished <- cmd.Wait() 248 }() 249 250 for { 251 select { 252 case err := <-finished: 253 if err != nil { 254 var suffix string 255 if isTerminated() { 256 suffix = " (terminated)" 257 } else if isInterrupted() { 258 suffix = " (interrupted)" 259 } 260 err = fmt.Errorf("error during %s%s: %v", stepName, suffix, err) 261 } 262 resChan <- cmdExecResult{stepName: stepName, output: stdout.String(), execTime: time.Since(start), err: err} 263 return 264 265 case <-termChan: 266 syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 267 if err := cmd.Process.Kill(); err != nil { 268 log.Printf("Failed to terminate %s (terminated 15m after interrupt): %v", strings.Join(cmd.Args, " "), err) 269 } 270 271 case <-intChan: 272 log.Printf("Interrupt after %s timeout during %s. Will terminate in another 15m", timeout, strings.Join(cmd.Args, " ")) 273 if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGINT); err != nil { 274 log.Printf("Failed to interrupt %s. Will terminate immediately: %v", strings.Join(cmd.Args, " "), err) 275 syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM) 276 cmd.Process.Kill() 277 } 278 } 279 } 280 } 281 282 // execute multiple commands in parallel 283 func finishRunningParallel(cmds ...*exec.Cmd) error { 284 var wg sync.WaitGroup 285 resultChan := make(chan cmdExecResult, len(cmds)) 286 termChan := make(chan struct{}, len(cmds)) 287 intChan := make(chan struct{}, len(cmds)) 288 289 for _, cmd := range cmds { 290 wg.Add(1) 291 go func(cmd *exec.Cmd) { 292 defer wg.Done() 293 executeParallelCommand(cmd, resultChan, termChan, intChan) 294 }(cmd) 295 } 296 297 go func() { 298 wg.Wait() 299 close(resultChan) 300 }() 301 302 cmdFailed := false 303 for { 304 select { 305 case <-terminate.C: 306 termLock.Lock() 307 terminated = true 308 termLock.Unlock() 309 terminate.Reset(time.Duration(0)) 310 select { 311 case <-termChan: 312 default: 313 close(termChan) 314 } 315 316 case <-interrupt.C: 317 intLock.Lock() 318 interrupted = true 319 intLock.Unlock() 320 terminate.Reset(15 * time.Minute) 321 close(intChan) 322 323 case result, ok := <-resultChan: 324 if !ok { 325 if cmdFailed { 326 return fmt.Errorf("one or more commands failed") 327 } 328 return nil 329 } 330 log.Print(result.output) 331 if result.err != nil { 332 cmdFailed = true 333 } 334 log.Printf("Step '%s' finished in %s", result.stepName, result.execTime) 335 } 336 } 337 } 338 339 // return exec.Command(cmd, args...) while calling .StdinPipe().WriteString(input) 340 func inputCommand(input, cmd string, args ...string) (*exec.Cmd, error) { 341 c := exec.Command(cmd, args...) 342 w, e := c.StdinPipe() 343 if e != nil { 344 return nil, e 345 } 346 go func() { 347 if _, e = io.WriteString(w, input); e != nil { 348 log.Printf("Failed to write all %d chars to %s: %v", len(input), cmd, e) 349 } 350 if e = w.Close(); e != nil { 351 log.Printf("Failed to close stdin for %s: %v", cmd, e) 352 } 353 }() 354 return c, nil 355 } 356 357 // return cmd.Output(), potentially timing out in the process. 358 func output(cmd *exec.Cmd) ([]byte, error) { 359 var stdout bytes.Buffer 360 cmd.Stdout = &stdout 361 err := finishRunning(cmd) 362 return stdout.Bytes(), err 363 364 } 365 366 // gs://foo and bar becomes gs://foo/bar 367 func joinURL(urlPath, path string) (string, error) { 368 u, err := url.Parse(urlPath) 369 if err != nil { 370 return "", err 371 } 372 u.Path = filepath.Join(u.Path, path) 373 return u.String(), nil 374 } 375 376 // Chdir() to dir and return a function to cd back to Getwd() 377 func pushd(dir string) (func() error, error) { 378 old, err := os.Getwd() 379 if err != nil { 380 return nil, fmt.Errorf("failed to os.Getwd(): %v", err) 381 } 382 if err = os.Chdir(dir); err != nil { 383 return nil, err 384 } 385 return func() error { 386 return os.Chdir(old) 387 }, nil 388 } 389 390 // Push env=value and return a function that resets env 391 func pushEnv(env, value string) (func() error, error) { 392 prev, present := os.LookupEnv(env) 393 if err := os.Setenv(env, value); err != nil { 394 return nil, fmt.Errorf("could not set %s: %v", env, err) 395 } 396 return func() error { 397 if present { 398 return os.Setenv(env, prev) 399 } 400 return os.Unsetenv(env) 401 }, nil 402 } 403 404 // Option that was an ENV that is now a --flag 405 type migratedOption struct { 406 env string // env associated with --flag 407 option *string // Value of --flag 408 name string // --flag name 409 skipPush bool // Push option to env if false 410 } 411 412 // Read value from ENV if --flag unset, optionally pushing to ENV 413 func migrateOptions(m []migratedOption) error { 414 for _, s := range m { 415 if *s.option == "" { 416 // Jobs may not be using --foo instead of FOO just yet, so ease the transition 417 // TODO(fejta): require --foo instead of FOO 418 v := os.Getenv(s.env) // expected Getenv 419 if v != "" { 420 // Tell people to use --foo=blah instead of FOO=blah 421 log.Printf("Please use kubetest %s=%s (instead of deprecated %s=%s)", s.name, v, s.env, v) 422 *s.option = v 423 } 424 } 425 if s.skipPush { 426 continue 427 } 428 // Script called by kubetest may expect these values to be set, so set them 429 // TODO(fejta): refactor the scripts below kubetest to use explicit config 430 if *s.option == "" { 431 continue 432 } 433 if err := os.Setenv(s.env, *s.option); err != nil { 434 return fmt.Errorf("could not set %s=%s: %v", s.env, *s.option, err) 435 } 436 } 437 return nil 438 } 439 440 func appendField(fields []string, flag, prefix string) []string { 441 fields, cur, _ := extractField(fields, flag) 442 if len(cur) == 0 { 443 cur = prefix 444 } else { 445 cur = cur + "-" + prefix 446 } 447 return append(fields, flag+"="+cur) 448 } 449 450 func setFieldDefault(fields []string, flag, val string) []string { 451 fields, cur, present := extractField(fields, flag) 452 if !present { 453 cur = val 454 } 455 return append(fields, flag+"="+cur) 456 } 457 458 // extractField("--a=this --b=that --c=other", "--b") returns [--a=this, --c=other"], "that" 459 func extractField(fields []string, target string) ([]string, string, bool) { 460 f := []string{} 461 prefix := target + "=" 462 consumeNext := false 463 done := false 464 r := "" 465 for _, field := range fields { 466 switch { 467 case done: 468 f = append(f, field) 469 case consumeNext: 470 r = field 471 done = true 472 case field == target: 473 consumeNext = true 474 case strings.HasPrefix(field, prefix): 475 r = strings.SplitN(field, "=", 2)[1] 476 done = true 477 default: 478 f = append(f, field) 479 } 480 } 481 return f, r, done 482 } 483 484 // execError returns a string format of err including stderr if the 485 // err is an ExitError, useful for errors from e.g. exec.Cmd.Output(). 486 func execError(err error) string { 487 if ee, ok := err.(*exec.ExitError); ok { 488 return fmt.Sprintf("%v (output: %q)", err, string(ee.Stderr)) 489 } 490 return err.Error() 491 }