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  }