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