jx/internal/command/command.go

215 lines
5.7 KiB
Go
Raw Normal View History

2024-07-01 07:16:55 +00:00
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
_ "context"
2024-07-01 07:16:55 +00:00
"fmt"
2024-07-17 08:34:57 +00:00
"errors"
2024-07-01 07:16:55 +00:00
"io"
"log/slog"
_ "net/url"
2024-07-01 07:16:55 +00:00
"os/exec"
"strings"
"text/template"
"decl/internal/codec"
"syscall"
2024-07-01 07:16:55 +00:00
)
// A resource that implements the ExecProvider interface can be used as an exec target.
type CommandProvider interface {
Start() error
Wait() error
SetCmdEnv([]string)
SetStdin(io.Reader)
StdinPipe() (io.WriteCloser, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
}
2024-07-17 08:34:57 +00:00
var ErrUnknownCommand error = errors.New("Unable to find command in path")
2024-07-01 07:16:55 +00:00
type CommandExecutor func(value any) ([]byte, error)
type CommandExtractAttributes func(output []byte, target any) error
2024-07-17 08:34:57 +00:00
type CommandExists func() error
2024-07-01 07:16:55 +00:00
type CommandInput string
2024-07-01 07:16:55 +00:00
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:"-"`
TargetRef CommandTargetRef `json:"targetref,omitempty" yaml:"targetref,omitempty"`
execHandle CommandProvider `json:"-" yaml:"-"`
2024-07-01 07:16:55 +00:00
}
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
2024-07-17 08:34:57 +00:00
c.CommandExists = func() error {
if _, err := exec.LookPath(c.Path); err != nil {
return fmt.Errorf("%w - %w", ErrUnknownCommand, err)
}
return nil
}
2024-07-01 07:16:55 +00:00
c.Executor = func(value any) ([]byte, error) {
c.ClearOutput()
c.execHandle = c.TargetRef.Provider(c, value)
if inputErr := c.SetInput(value); inputErr != nil {
return nil, inputErr
}
c.SetCmdEnv()
cmd := c.execHandle
2024-09-19 05:32:22 +00:00
if c.stdin != nil {
cmd.SetStdin(c.stdin)
2024-09-19 05:32:22 +00:00
}
2024-07-01 07:16:55 +00:00
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)
2024-09-19 05:32:22 +00:00
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))
}
2024-07-01 07:16:55 +00:00
waitErr := cmd.Wait()
c.Stdout = string(stdOutOutput)
c.Stderr = string(stdErrOutput)
c.ExitCode = c.GetExitCodeFromError(waitErr)
/*
2024-09-19 05:32:22 +00:00
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))
}
*/
2024-07-01 07:16:55 +00:00
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() {
c.execHandle.SetCmdEnv(c.Env)
2024-07-01 07:16:55 +00:00
}
2024-09-19 05:32:22 +00:00
func (c *Command) SetStdinReader(r io.Reader) {
if c.StdinAvailable {
c.stdin = r
}
}
2024-07-01 07:16:55 +00:00
func (c *Command) Exists() bool {
2024-07-17 08:34:57 +00:00
return c.CommandExists() == nil
2024-07-01 07:16:55 +00:00
}
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
}
2024-07-01 07:16:55 +00:00
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 {
slog.Info("Command.SetInput", "input", r.String(), "error", err)
return err
} else {
slog.Info("Command.SetInput", "input", r.String())
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
}