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  }