224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
|
|
|
package command
|
|
|
|
import (
|
|
_ "context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"errors"
|
|
"gopkg.in/yaml.v3"
|
|
"io"
|
|
"log/slog"
|
|
_ "net/url"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"text/template"
|
|
"decl/internal/codec"
|
|
"syscall"
|
|
)
|
|
|
|
var ErrUnknownCommand error = errors.New("Unable to find command in path")
|
|
|
|
type CommandExecutor func(value any) ([]byte, error)
|
|
type CommandExtractAttributes func(output []byte, target any) error
|
|
type CommandExists func() error
|
|
|
|
type CommandArg string
|
|
|
|
type CommandInput string
|
|
|
|
type Command struct {
|
|
Path string `json:"path" yaml:"path"`
|
|
Args []CommandArg `json:"args" yaml:"args"`
|
|
Env []string `json:"env" yaml:"env"`
|
|
Split bool `json:"split" yaml:"split"`
|
|
FailOnError bool `json:"failonerror" yaml:"failonerror"`
|
|
StdinAvailable bool `json:"stdinavailable,omitempty" yaml:"stdinavailable,omitempty"`
|
|
ExitCode int `json:"exitcode,omitempty" yaml:"exitcode,omitempty"`
|
|
Stdout string `json:"stdout,omitempty" yaml:"stdout,omitempty"`
|
|
Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
|
|
Executor CommandExecutor `json:"-" yaml:"-"`
|
|
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
|
|
CommandExists CommandExists `json:"-" yaml:"-"`
|
|
Input CommandInput `json:"-" yaml:"-"`
|
|
stdin io.Reader `json:"-" yaml:"-"`
|
|
}
|
|
|
|
func NewCommand() *Command {
|
|
c := &Command{ Split: true, FailOnError: true }
|
|
c.Defaults()
|
|
return c
|
|
}
|
|
|
|
func (c *Command) ClearOutput() {
|
|
c.Stdout = ""
|
|
c.Stderr = ""
|
|
c.ExitCode = 0
|
|
}
|
|
|
|
func (c *Command) Defaults() {
|
|
c.ClearOutput()
|
|
c.Split = true
|
|
c.FailOnError = true
|
|
c.CommandExists = func() error {
|
|
if _, err := exec.LookPath(c.Path); err != nil {
|
|
return fmt.Errorf("%w - %w", ErrUnknownCommand, err)
|
|
}
|
|
return nil
|
|
}
|
|
c.Executor = func(value any) ([]byte, error) {
|
|
c.ClearOutput()
|
|
args, err := c.Template(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if inputErr := c.SetInput(value); inputErr != nil {
|
|
return nil, inputErr
|
|
}
|
|
cmd := exec.Command(c.Path, args...)
|
|
c.SetCmdEnv(cmd)
|
|
|
|
if c.stdin != nil {
|
|
cmd.Stdin = c.stdin
|
|
}
|
|
|
|
slog.Info("execute() - cmd", "path", c.Path, "args", args)
|
|
output, stdoutPipeErr := cmd.StdoutPipe()
|
|
if stdoutPipeErr != nil {
|
|
return nil, stdoutPipeErr
|
|
}
|
|
|
|
stderr, pipeErr := cmd.StderrPipe()
|
|
if pipeErr != nil {
|
|
return nil, pipeErr
|
|
}
|
|
|
|
if startErr := cmd.Start(); startErr != nil {
|
|
return nil, startErr
|
|
}
|
|
|
|
slog.Info("execute() - start", "cmd", cmd)
|
|
stdOutOutput, _ := io.ReadAll(output)
|
|
stdErrOutput, _ := io.ReadAll(stderr)
|
|
if len(stdOutOutput) > 100 {
|
|
slog.Info("execute() - io", "stdout", string(stdOutOutput[:100]), "stderr", string(stdErrOutput))
|
|
} else {
|
|
slog.Info("execute() - io", "stdout", string(stdOutOutput), "stderr", string(stdErrOutput))
|
|
}
|
|
waitErr := cmd.Wait()
|
|
|
|
c.Stdout = string(stdOutOutput)
|
|
c.Stderr = string(stdErrOutput)
|
|
c.ExitCode = c.GetExitCodeFromError(waitErr)
|
|
|
|
if len(stdOutOutput) > 100 {
|
|
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput[:100]), "error", string(stdErrOutput))
|
|
} else {
|
|
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput))
|
|
}
|
|
|
|
if len(stdErrOutput) > 0 && c.FailOnError {
|
|
return stdOutOutput, fmt.Errorf("%w %s", waitErr, string(stdErrOutput))
|
|
}
|
|
return stdOutOutput, waitErr
|
|
}
|
|
}
|
|
|
|
func (c *Command) Load(r io.Reader) error {
|
|
return codec.NewYAMLDecoder(r).Decode(c)
|
|
}
|
|
|
|
func (c *Command) LoadDecl(yamlResourceDeclaration string) error {
|
|
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c)
|
|
}
|
|
|
|
func (c *Command) SetCmdEnv(cmd *exec.Cmd) {
|
|
cmd.Env = append(os.Environ(), c.Env...)
|
|
}
|
|
|
|
func (c *Command) SetStdinReader(r io.Reader) {
|
|
if c.StdinAvailable {
|
|
c.stdin = r
|
|
}
|
|
}
|
|
|
|
func (c *Command) Exists() bool {
|
|
return c.CommandExists() == nil
|
|
}
|
|
|
|
func (c *Command) GetExitCodeFromError(err error) (ec int) {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
|
return status.ExitStatus()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Command) Template(value any) ([]string, error) {
|
|
var args []string = make([]string, 0, len(c.Args) * 2)
|
|
for i, arg := range c.Args {
|
|
var commandLineArg strings.Builder
|
|
err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if commandLineArg.Len() > 0 {
|
|
var splitArg []string
|
|
if c.Split {
|
|
splitArg = strings.Split(commandLineArg.String(), " ")
|
|
} else {
|
|
splitArg = []string{commandLineArg.String()}
|
|
}
|
|
slog.Info("Template()", "split", splitArg, "len", len(splitArg))
|
|
args = append(args, splitArg...)
|
|
}
|
|
}
|
|
|
|
slog.Info("Template()", "Args", c.Args, "lencargs", len(c.Args), "args", args, "lenargs", len(args), "value", value)
|
|
return args, nil
|
|
}
|
|
|
|
func (c *Command) Execute(value any) ([]byte, error) {
|
|
return c.Executor(value)
|
|
}
|
|
|
|
func (c *Command) SetInput(value any) error {
|
|
if len(c.Input) > 0 {
|
|
if r, err := c.Input.Template(value); err != nil {
|
|
return err
|
|
} else {
|
|
c.SetStdinReader(strings.NewReader(r.String()))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *CommandInput) Template(value any) (result strings.Builder, err error) {
|
|
err = template.Must(template.New("commandInput").Parse(string(*c))).Execute(&result, value)
|
|
return
|
|
}
|
|
|
|
func (c *CommandArg) UnmarshalValue(value string) error {
|
|
*c = CommandArg(value)
|
|
return nil
|
|
}
|
|
|
|
func (c *CommandArg) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
|
|
return unmarshalRouteTypeErr
|
|
}
|
|
return c.UnmarshalValue(s)
|
|
}
|
|
|
|
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
|
|
var s string
|
|
if err := value.Decode(&s); err != nil {
|
|
return err
|
|
}
|
|
return c.UnmarshalValue(s)
|
|
}
|