github.com/jenkins-x/jx-api@v0.0.24/pkg/util/commands.go (about) 1 package util 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "os/exec" 8 "strings" 9 "time" 10 11 "github.com/pkg/errors" 12 ) 13 14 // Command is a struct containing the details of an external command to be executed 15 type Command struct { 16 attempts int 17 Errors []error 18 Dir string 19 Name string 20 Args []string 21 Timeout time.Duration 22 Out io.Writer 23 Err io.Writer 24 In io.Reader 25 Env map[string]string 26 } 27 28 // CommandError is the error object encapsulating an error from a Command 29 type CommandError struct { 30 Command Command 31 Output string 32 cause error 33 } 34 35 func (c CommandError) Error() string { 36 // sanitise any password arguments before printing the error string. The actual sensitive argument is still present 37 // in the Command object 38 sanitisedArgs := make([]string, len(c.Command.Args)) 39 copy(sanitisedArgs, c.Command.Args) 40 for i, arg := range sanitisedArgs { 41 if strings.Contains(strings.ToLower(arg), "password") && i < len(sanitisedArgs)-1 { 42 // sanitise the subsequent argument to any 'password' fields 43 sanitisedArgs[i+1] = "*****" 44 } 45 } 46 47 return fmt.Sprintf("failed to run '%s %s' command in directory '%s', output: '%s'", 48 c.Command.Name, strings.Join(sanitisedArgs, " "), c.Command.Dir, c.Output) 49 } 50 51 func (c CommandError) Cause() error { 52 return c.cause 53 } 54 55 // SetName Setter method for Name to enable use of interface instead of Command struct 56 func (c *Command) SetName(name string) { 57 c.Name = name 58 } 59 60 // CurrentName returns the current name of the command 61 func (c *Command) CurrentName() string { 62 return c.Name 63 } 64 65 // SetDir Setter method for Dir to enable use of interface instead of Command struct 66 func (c *Command) SetDir(dir string) { 67 c.Dir = dir 68 } 69 70 // CurrentDir returns the current Dir 71 func (c *Command) CurrentDir() string { 72 return c.Dir 73 } 74 75 // SetArgs Setter method for Args to enable use of interface instead of Command struct 76 func (c *Command) SetArgs(args []string) { 77 c.Args = args 78 } 79 80 // CurrentArgs returns the current command arguments 81 func (c *Command) CurrentArgs() []string { 82 return c.Args 83 } 84 85 // SetTimeout Setter method for Timeout to enable use of interface instead of Command struct 86 func (c *Command) SetTimeout(timeout time.Duration) { 87 c.Timeout = timeout 88 } 89 90 // SetEnv Setter method for Env to enable use of interface instead of Command struct 91 func (c *Command) SetEnv(env map[string]string) { 92 c.Env = env 93 } 94 95 // CurrentEnv returns the current environment variables 96 func (c *Command) CurrentEnv() map[string]string { 97 return c.Env 98 } 99 100 // SetEnvVariable sets an environment variable into the environment 101 func (c *Command) SetEnvVariable(name string, value string) { 102 if c.Env == nil { 103 c.Env = map[string]string{} 104 } 105 c.Env[name] = value 106 } 107 108 // Attempts The number of times the command has been executed 109 func (c *Command) Attempts() int { 110 return c.attempts 111 } 112 113 // DidError returns a boolean if any error occurred in any execution of the command 114 func (c *Command) DidError() bool { 115 return len(c.Errors) > 0 116 } 117 118 // DidFail returns a boolean if the command could not complete (errored on every attempt) 119 func (c *Command) DidFail() bool { 120 return len(c.Errors) == c.attempts 121 } 122 123 // Error returns the last error 124 func (c *Command) Error() error { 125 if len(c.Errors) > 0 { 126 return c.Errors[len(c.Errors)-1] 127 } 128 return nil 129 } 130 131 // RunWithoutRetry Execute the command without retrying on failure and block waiting for return values 132 func (c *Command) RunWithoutRetry() (string, error) { 133 err := os.Setenv("PATH", PathWithBinary(c.Dir)) 134 if err != nil { 135 return "", errors.Wrap(err, "failed to set PATH env variable") 136 } 137 var r string 138 var e error 139 140 r, e = c.run() 141 c.attempts++ 142 if e != nil { 143 c.Errors = append(c.Errors, e) 144 } 145 return r, e 146 } 147 148 func (c *Command) String() string { 149 var builder strings.Builder 150 for k, v := range c.Env { 151 builder.WriteString(k) 152 builder.WriteString("=") 153 builder.WriteString(v) 154 builder.WriteString(" ") 155 } 156 builder.WriteString(c.Name) 157 for _, arg := range c.Args { 158 builder.WriteString(" ") 159 builder.WriteString(arg) 160 } 161 return builder.String() 162 } 163 164 func (c *Command) run() (string, error) { 165 e := exec.Command(c.Name, c.Args...) // #nosec 166 if c.Dir != "" { 167 e.Dir = c.Dir 168 } 169 if len(c.Env) > 0 { 170 m := map[string]string{} 171 environ := os.Environ() 172 for _, kv := range environ { 173 paths := strings.SplitN(kv, "=", 2) 174 if len(paths) == 2 { 175 m[paths[0]] = paths[1] 176 } 177 } 178 for k, v := range c.Env { 179 m[k] = v 180 } 181 envVars := []string{} 182 for k, v := range m { 183 envVars = append(envVars, k+"="+v) 184 } 185 e.Env = envVars 186 } 187 188 if c.Out != nil { 189 e.Stdout = c.Out 190 } 191 192 if c.Err != nil { 193 e.Stderr = c.Err 194 } 195 196 if c.In != nil { 197 e.Stdin = c.In 198 } 199 200 var text string 201 var err error 202 203 if c.Out != nil { 204 err := e.Run() 205 if err != nil { 206 return text, CommandError{ 207 Command: *c, 208 cause: err, 209 } 210 } 211 } else { 212 data, err := e.CombinedOutput() 213 output := string(data) 214 text = strings.TrimSpace(output) 215 if err != nil { 216 return text, CommandError{ 217 Command: *c, 218 Output: text, 219 cause: err, 220 } 221 } 222 } 223 224 return text, err 225 } 226 227 //PathWithBinary Returns the path to be used to execute a binary from, takes the form JX_HOME/bin:mvnBinDir:customPaths 228 func PathWithBinary(customPaths ...string) string { 229 existingEnvironmentPath := os.Getenv("PATH") 230 231 extraPaths := "" 232 for _, p := range customPaths { 233 if p != "" { 234 extraPaths += string(os.PathListSeparator) + p 235 } 236 } 237 238 jxBinDir, _ := JXBinLocation() 239 return jxBinDir + string(os.PathListSeparator) + existingEnvironmentPath + extraPaths 240 }