/
exec.go
108 lines (92 loc) · 3.08 KB
/
exec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package exec contains an interface for executing commands, along with helpers
// TODO(bentheelder): add standardized timeout functionality & a default timeout
// so that commands cannot hang indefinitely (!)
// ref: https://github.com/kubernetes-sigs/kind
package exec
import (
"bufio"
"bytes"
"io"
"os"
"time"
log "github.com/sirupsen/logrus"
)
// Cmd abstracts over running a command somewhere, this is useful for testing
type Cmd interface {
Run() error
// Each entry should be of the form "key=value"
SetEnv(...string) Cmd
SetStdin(io.Reader) Cmd
SetStdout(io.Writer) Cmd
SetStderr(io.Writer) Cmd
}
// Cmder abstracts over creating commands
type Cmder interface {
// command, args..., just like os/exec.Cmd
Command(string, ...string) Cmd
}
// DefaultCmder is a LocalCmder instance used for convienience, packages
// originally using os/exec.Command can instead use pkg/kind/exec.Command
// which forwards to this instance
// TODO(bentheelder): swap this for testing
// TODO(bentheelder): consider not using a global for this :^)
var DefaultCmder = &LocalCmder{}
// Command is a convience wrapper over DefaultCmder.Command
func Command(command string, args ...string) Cmd {
return DefaultCmder.Command(command, args...)
}
// CombinedOutputLines is like os/exec's cmd.CombinedOutput(),
// but over our Cmd interface, and instead of returning the byte buffer of
// stderr + stdout, it scans these for lines and returns a slice of output lines
func CombinedOutputLines(cmd Cmd) (lines []string, err error) {
var buff bytes.Buffer
cmd.SetStdout(&buff)
cmd.SetStderr(&buff)
err = cmd.Run()
scanner := bufio.NewScanner(&buff)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, err
}
// InheritOutput sets cmd's output to write to the current process's stdout and stderr
func InheritOutput(cmd Cmd) {
cmd.SetStderr(os.Stderr)
cmd.SetStdout(os.Stdout)
}
// RunLoggingOutputOnFail runs the cmd, logging error output if Run returns an error
func RunLoggingOutputOnFail(cmd Cmd, retries int) error {
var buff bytes.Buffer
cmd.SetStdout(&buff)
cmd.SetStderr(&buff)
err := cmd.Run()
if err != nil {
// retry pulling up to retries times if necessary
for i := 0; i < retries; i++ {
time.Sleep(time.Second * time.Duration(i+1))
err = cmd.Run()
if err == nil {
return nil
}
}
// All retries failed or none were requested
log.Errorf("failed with following error after %d retries:", retries)
scanner := bufio.NewScanner(&buff)
for scanner.Scan() {
log.Error(scanner.Text())
}
}
return err
}