hack/deployer/exec/cmd.go (138 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package exec import ( "bytes" "context" "fmt" "io" "log" "os" "os/exec" "strings" "text/template" "time" ) // Command allows building commands to execute using fluent-style api type Command struct { command string context context.Context logPrefix string params map[string]interface{} variablesSrc string variables []string stream bool stderr bool } func NewCommand(command string) *Command { return &Command{command: command, stream: true, stderr: true} } func (c *Command) AsTemplate(params map[string]interface{}) *Command { c.params = params return c } func (c *Command) WithVariable(name, value string) *Command { c.variables = append(c.variables, name+"="+value) return c } func (c *Command) WithVariablesFromFile(filename string) *Command { c.variablesSrc = filename return c } func (c *Command) WithContext(ctx context.Context) *Command { c.context = ctx return c } func (c *Command) WithLog(logPrefix string) *Command { c.logPrefix = logPrefix return c } func (c *Command) WithoutStreaming() *Command { c.stream = false return c } func (c *Command) StdoutOnly() *Command { c.stderr = false return c } func (c *Command) Run() error { _, err := c.output() return err } func (c *Command) RunWithRetries(numAttempts int, timeout time.Duration) error { var err error for i := 0; i < numAttempts; i++ { ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) err = c.WithContext(ctx).Run() cancelFunc() if err == nil { return nil } } return err } func (c *Command) Output() (string, error) { return c.output() } func (c *Command) OutputContainsAny(tokens ...string) (bool, error) { out, err := c.output() for _, token := range tokens { if strings.Contains(out, token) { return true, err } } if err != nil { // provide additional context to callers otherwise it is really hard to figure out what went wrong err = fmt.Errorf("%s with err: %w", out, err) } return false, err } func (c *Command) OutputList() (list []string, err error) { out, err := c.output() if err != nil { return nil, err } for _, item := range strings.Split(out, "\n") { if item != "" { list = append(list, item) } } return } func (c *Command) output() (string, error) { if c.params != nil { var b bytes.Buffer if err := template.Must(template.New(""). Funcs(template.FuncMap{"Join": strings.Join}). Parse(c.command)).Execute(&b, c.params); err != nil { return "", err } c.command = b.String() } if c.logPrefix != "" { log.Printf("%s: %s", c.logPrefix, c.command) } var cmd *exec.Cmd if c.context != nil { cmd = exec.CommandContext(c.context, "/usr/bin/env", "bash", "-c", c.command) // #nosec G204 } else { cmd = exec.Command("/usr/bin/env", "bash", "-c", c.command) // #nosec G204 } // support .env or similar files to specify environment variables if c.variablesSrc != "" { bytes, err := os.ReadFile(c.variablesSrc) if err != nil { return "", err } // assume k=v pair lines c.variables = append(c.variables, strings.Split(string(bytes), "\n")...) } cmd.Env = append(os.Environ(), c.variables...) b := bytes.Buffer{} if c.stream { cmd.Stdout = io.MultiWriter(os.Stdout, &b) cmd.Stderr = io.MultiWriter(os.Stderr, &b) } else { cmd.Stdout = &b cmd.Stderr = &b } if !c.stderr { cmd.Stderr = nil } err := cmd.Run() return b.String(), err }