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  }