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