diff --git a/go.mod b/go.mod index 8779e3d5..1b38db99 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,7 @@ module github.com/netobserv/network-observability-cli go 1.22.3 - -toolchain go1.22.4 +toolchain go1.23.3 require ( github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 @@ -68,7 +67,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/segmentio/kafka-go v0.4.47 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/vladimirvivien/gexe v0.3.0 + github.com/vladimirvivien/gexe v0.4.1 github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/go.sum b/go.sum index 99b1585e..04220606 100644 --- a/go.sum +++ b/go.sum @@ -187,8 +187,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vladimirvivien/gexe v0.3.0 h1:4xwiOwGrDob5OMR6E92B9olDXYDglXdHhzR1ggYtWJM= -github.com/vladimirvivien/gexe v0.3.0/go.mod h1:fp7cy60ON1xjhtEI/+bfSEIXX35qgmI+iRYlGOqbBFM= +github.com/vladimirvivien/gexe v0.4.1 h1:W9gWkp8vSPjDoXDu04Yp4KljpVMaSt8IQuHswLDd5LY= +github.com/vladimirvivien/gexe v0.4.1/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= diff --git a/vendor/github.com/vladimirvivien/gexe/TODO.md b/vendor/github.com/vladimirvivien/gexe/TODO.md index 022f44d1..fd3014dc 100644 --- a/vendor/github.com/vladimirvivien/gexe/TODO.md +++ b/vendor/github.com/vladimirvivien/gexe/TODO.md @@ -1,8 +1,10 @@ -# Important tasks +# TODOs The following are high-level tasks that will be considered for upcoming release cycles. +## Top features/tasks * [ ] Support and test on Windows platform (#27) * [ ] Ability to map and access program flags (#20) +* [ ] Support for SSH/SCP * [x] Provide a simple API to submit HTTP requests and retrieve HTTP documents (think of wget/curl) * [x] Support for scatter/gather exec commands * [x] Support for concurrent exec of os commands diff --git a/vendor/github.com/vladimirvivien/gexe/exec/builder.go b/vendor/github.com/vladimirvivien/gexe/exec/builder.go index c1b41f90..c5395c88 100644 --- a/vendor/github.com/vladimirvivien/gexe/exec/builder.go +++ b/vendor/github.com/vladimirvivien/gexe/exec/builder.go @@ -1,7 +1,9 @@ package exec import ( + "context" "fmt" + "io" "sync" "github.com/vladimirvivien/gexe/vars" @@ -61,6 +63,7 @@ type PipedCommandResult struct { procs []*Proc errProcs []*Proc lastProc *Proc + err error } // Procs return all executed processes in pipe @@ -105,26 +108,37 @@ type CommandBuilder struct { cmdPolicy CommandPolicy procs []*Proc vars *vars.Variables + err error + stdout io.Writer + stderr io.Writer } -// Commands creates a *CommandBuilder used to collect -// command strings to be executed. -func Commands(cmds ...string) *CommandBuilder { +// CommandsWithContextVars creates a *CommandBuilder with the specified context and session variables. +// The resulting *CommandBuilder is used to execute command strings. +func CommandsWithContextVars(ctx context.Context, variables *vars.Variables, cmds ...string) *CommandBuilder { cb := new(CommandBuilder) - cb.vars = &vars.Variables{} + cb.vars = variables for _, cmd := range cmds { - cb.procs = append(cb.procs, NewProc(cmd)) + cb.procs = append(cb.procs, NewProcWithContextVars(ctx, cmd, variables)) } return cb } +// CommandsWithContext creates a *CommandBuilder, with specified context, used to collect +// command strings to be executed. +func CommandsWithContext(ctx context.Context, cmds ...string) *CommandBuilder { + return CommandsWithContextVars(ctx, &vars.Variables{}, cmds...) +} + +// Commands creates a *CommandBuilder used to collect +// command strings to be executed. +func Commands(cmds ...string) *CommandBuilder { + return CommandsWithContext(context.Background(), cmds...) +} + // CommandsWithVars creates a new CommandBuilder and sets session varialbes for it func CommandsWithVars(variables *vars.Variables, cmds ...string) *CommandBuilder { - cb := &CommandBuilder{vars: variables} - for _, cmd := range cmds { - cb.procs = append(cb.procs, NewProc(variables.Eval(cmd))) - } - return cb + return CommandsWithContextVars(context.Background(), variables, cmds...) } // WithPolicy sets one or more command policy mask values, i.e. (CmdOnErrContinue | CmdExecConcurrent) @@ -141,6 +155,26 @@ func (cb *CommandBuilder) Add(cmds ...string) *CommandBuilder { return cb } +// WithStdout sets the standard output stream for the builder +func (cb *CommandBuilder) WithStdout(out io.Writer) *CommandBuilder { + cb.stdout = out + return cb +} + +// WithStderr sets the standard output err stream for the builder +func (cb *CommandBuilder) WithStderr(err io.Writer) *CommandBuilder { + cb.stderr = err + return cb +} + +// WithWorkDir sets the working directory for all defined commands +func (cb *CommandBuilder) WithWorkDir(dir string) *CommandBuilder { + for _, proc := range cb.procs { + proc.cmd.Dir = dir + } + return cb +} + // Run executes all commands successively and waits for all of the result. The result of each individual // command can be accessed from CommandResult.Procs[] after the execution completes. If policy == ExitOnErrPolicy, the // execution will stop on the first error encountered, otherwise it will continue. Processes with errors can be accessed @@ -170,15 +204,24 @@ func (cb *CommandBuilder) Start() *CommandResult { go func(builder *CommandBuilder, cr *CommandResult) { defer close(cr.workChan) - // start with concurrently + // start with concurrently and wait for all procs to launch if hasPolicy(builder.cmdPolicy, ConcurrentExecPolicy) { var gate sync.WaitGroup for _, proc := range builder.procs { cr.mu.Lock() cr.procs = append(cr.procs, proc) cr.mu.Unlock() - proc.cmd.Stdout = proc.result - proc.cmd.Stderr = proc.result + + // setup standard output/err + proc.cmd.Stdout = cb.stdout + if cb.stdout == nil { + proc.cmd.Stdout = proc.result + } + + proc.cmd.Stderr = cb.stderr + if cb.stderr == nil { + proc.cmd.Stderr = proc.result + } gate.Add(1) go func(conProc *Proc, conResult *CommandResult) { @@ -201,8 +244,17 @@ func (cb *CommandBuilder) Start() *CommandResult { cr.mu.Lock() cr.procs = append(cr.procs, proc) cr.mu.Unlock() - proc.cmd.Stdout = proc.result - proc.cmd.Stderr = proc.result + + // setup standard output/err + proc.cmd.Stdout = cb.stdout + if cb.stdout == nil { + proc.cmd.Stdout = proc.result + } + + proc.cmd.Stderr = cb.stderr + if cb.stderr == nil { + proc.cmd.Stderr = proc.result + } // start sequentially if err := proc.Start().Err(); err != nil { @@ -231,25 +283,40 @@ func (cb *CommandBuilder) Concurr() *CommandResult { // Pipe executes each command serially chaining the combinedOutput of previous command to the inputPipe of next command. func (cb *CommandBuilder) Pipe() *PipedCommandResult { + if cb.err != nil { + return &PipedCommandResult{err: cb.err} + } + var result PipedCommandResult procLen := len(cb.procs) if procLen == 0 { - return nil + return &PipedCommandResult{} } + // wire last proc to combined output last := procLen - 1 result.lastProc = cb.procs[last] - result.lastProc.cmd.Stdout = result.lastProc.result - result.lastProc.cmd.Stderr = result.lastProc.result - if procLen > 1 { + // setup standard output/err for last proc in pipe + result.lastProc.cmd.Stdout = cb.stdout + if cb.stdout == nil { result.lastProc.cmd.Stdout = result.lastProc.result - // connect input/output between commands - for i, p := range cb.procs[:last] { - // link proc.Output to proc[next].Input - cb.procs[i+1].SetStdin(p.GetOutputPipe()) - p.cmd.Stderr = p.result + } + + result.lastProc.cmd.Stderr = cb.stderr + if cb.stderr == nil { + result.lastProc.cmd.Stderr = result.lastProc.result + } + + result.lastProc.cmd.Stdout = result.lastProc.result + for i, p := range cb.procs[:last] { + pipeout, err := p.cmd.StdoutPipe() + if err != nil { + p.err = err + return &PipedCommandResult{err: err, errProcs: []*Proc{p}} } + + cb.procs[i+1].cmd.Stdin = pipeout } // start each process (but, not wait for result) @@ -273,10 +340,18 @@ func (cb *CommandBuilder) Pipe() *PipedCommandResult { return &result } -func (cp *CommandBuilder) runCommand(proc *Proc) error { - // setup combined output for reach proc - proc.cmd.Stdout = proc.result - proc.cmd.Stderr = proc.result +func (cb *CommandBuilder) runCommand(proc *Proc) error { + // setup standard out and standard err + + proc.cmd.Stdout = cb.stdout + if cb.stdout == nil { + proc.cmd.Stdout = proc.result + } + + proc.cmd.Stderr = cb.stderr + if cb.stderr == nil { + proc.cmd.Stderr = proc.result + } if err := proc.Start().Err(); err != nil { return err diff --git a/vendor/github.com/vladimirvivien/gexe/exec/proc.go b/vendor/github.com/vladimirvivien/gexe/exec/proc.go index a91db489..f9b6a520 100644 --- a/vendor/github.com/vladimirvivien/gexe/exec/proc.go +++ b/vendor/github.com/vladimirvivien/gexe/exec/proc.go @@ -2,11 +2,15 @@ package exec import ( "bytes" + "context" "fmt" "io" "os" osexec "os/exec" + "os/user" + "strconv" "strings" + "syscall" "time" "github.com/vladimirvivien/gexe/vars" @@ -16,6 +20,8 @@ import ( type Proc struct { id int err error + userid *int + groupid *int state *os.ProcessState result *bytes.Buffer outputPipe io.ReadCloser @@ -26,104 +32,145 @@ type Proc struct { vars *vars.Variables } -// NewProc sets up command string to be started as an OS process, however -// does not start the process. -func NewProc(cmdStr string) *Proc { +// NewProcWithContext sets up command string to be started as an OS process using the specified context. +// However, it does not start the process. The process must be started using a subsequent call to +// Proc.StartXXX() or Proc.RunXXX() method. +func NewProcWithContext(ctx context.Context, cmdStr string) *Proc { words, err := parse(cmdStr) if err != nil { return &Proc{err: err} } - command := osexec.Command(words[0], words[1:]...) - pipeout, outerr := command.StdoutPipe() - pipeerr, errerr := command.StderrPipe() + command := osexec.CommandContext(ctx, words[0], words[1:]...) - if outerr != nil || errerr != nil { - return &Proc{err: fmt.Errorf("combinedOutput pipe: %s; %s", outerr, errerr)} + return &Proc{ + cmd: command, + result: new(bytes.Buffer), + vars: &vars.Variables{}, } - pipein, inerr := command.StdinPipe() - if inerr != nil { - return &Proc{err: fmt.Errorf("inputPipe err: %w", inerr)} - } +} - return &Proc{ - cmd: command, - outputPipe: pipeout, - errorPipe: pipeerr, - inputPipe: pipein, - result: new(bytes.Buffer), - vars: &vars.Variables{}, - } +// NewProc sets up command string to be started as an OS process, however +// does not start the process. The process must be started using a subsequent call to +// Proc.StartXXX() or Proc.RunXXX() method. +func NewProc(cmdStr string) *Proc { + return NewProcWithContext(context.Background(), cmdStr) } // NewProcWithVars sets up new command string and session variables for a new proc func NewProcWithVars(cmdStr string, variables *vars.Variables) *Proc { - p := NewProc(variables.Eval(cmdStr)) + p := NewProcWithContext(context.Background(), variables.Eval(cmdStr)) p.vars = variables return p } -// StartProc starts an OS process (setup a combined output of stdout, stderr) and does not wait for -// it to complete. You must follow this with either proc.Wait() to wait for result directly. Otherwise, -// call proc.Out() or proc.Result() which automatically waits and gather result. -func StartProc(cmdStr string) *Proc { - proc := NewProc(cmdStr) +// NewProcWithContextVars is a convenient function to create new Proc with context and variables. +func NewProcWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) *Proc { + proc := NewProcWithContext(ctx, variables.Eval(cmdStr)) + proc.vars = variables + return proc +} + +// StartProcWithContext creates and starts an OS process (with combined stdout/stderr) using the specified context. +// The function does not wait for the process to complete and must be followed by proc.Wait() to wait for process completion. +// Then, call proc.Out() or proc.Result() to access the process' result. +func StartProcWithContext(ctx context.Context, cmdStr string) *Proc { + proc := NewProcWithContext(ctx, cmdStr) + proc.cmd.Stdout = proc.result + proc.cmd.Stderr = proc.result + if proc.Err() != nil { return proc } - proc.cmd.Stdout = proc.result - proc.cmd.Stderr = proc.result return proc.Start() } -// StartProcWithVars sets session variables and calls StartProc +// StartProc creates and starts an OS process using StartProcWithContext using a default context. +func StartProc(cmdStr string) *Proc { + return StartProcWithContext(context.Background(), cmdStr) +} + +// StartProcWithVars sets session variables and calls StartProc to create and start a process. func StartProcWithVars(cmdStr string, variables *vars.Variables) *Proc { - proc := StartProc(variables.Eval(cmdStr)) + proc := StartProcWithContext(context.Background(), variables.Eval(cmdStr)) proc.vars = variables return proc } -// RunProc starts a new process and waits for its completion. Use Proc.Out() or Proc.Result() -// to access the combined result from stdout and stderr. -func RunProc(cmdStr string) *Proc { - proc := StartProc(cmdStr) - if proc.Err() != nil { +// StartProcWithContextVars is a convenient function that creates and starts a process with a context and variables. +func StartProcWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) *Proc { + proc := StartProcWithContext(ctx, variables.Eval(cmdStr)) + proc.vars = variables + return proc +} + +// RunProcWithContext creates, starts, and runs an OS process using the specified context. +// It then waits for a new process (with combined stdout/stderr) to complete. +// Use Proc.Out() to access the command's output as an io.Reader, or use Proc.Result() +// to access the commands output as a string. +func RunProcWithContext(ctx context.Context, cmdStr string) *Proc { + proc := StartProcWithContext(ctx, cmdStr) + if err := proc.Err(); err != nil { + return proc + } + if err := proc.Wait().Err(); err != nil { + proc.err = err return proc } - proc.Out() return proc } +// RunProc creates, starts, and runs for a new process using RunProcWithContext with a default context. +func RunProc(cmdStr string) *Proc { + return RunProcWithContext(context.Background(), cmdStr) +} + // RunProcWithVars sets session variables and calls RunProc func RunProcWithVars(cmdStr string, variables *vars.Variables) *Proc { - proc := RunProc(variables.Eval(cmdStr)) + proc := RunProcWithContext(context.Background(), variables.Eval(cmdStr)) + proc.vars = variables + return proc +} + +// RunProcWithContextVars runs a process with a context and session variables +func RunProcWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) *Proc { + proc := RunProcWithContext(ctx, variables.Eval(cmdStr)) proc.vars = variables return proc } -// Run creates and runs a process and waits for its result (combined stdin,stderr) returned as a string value. -// This is equivalent to calling Proc.RunProc() followed by Proc.Result(). +// RunWithContext creates and runs a new process using the specified context. +// It waits for its result (combined stdin,stderr) and makes it availble as a string value. +// This is equivalent to calling Proc.RunProcWithContext() followed by Proc.Result(). +func RunWithContext(ctx context.Context, cmdStr string) string { + return RunProcWithContext(ctx, cmdStr).Result() +} + +// Runs is a convenient shortcut to calling RunWithContext with a default context func Run(cmdStr string) (result string) { - return RunProc(cmdStr).Result() + return RunProcWithContext(context.Background(), cmdStr).Result() } -// RunWithVars sets session variables and call Run +// RunWithVars creates and runs a new process with a specified session variables. func RunWithVars(cmdStr string, variables *vars.Variables) string { return RunProcWithVars(cmdStr, variables).Result() } -// SetVars sets session variables for Proc -func (p *Proc) SetVars(variables *vars.Variables) *Proc { - p.vars = variables - return p +// RunWithContextVars creates and runs a new process with a specified context and session variables. +func RunWithContextVars(ctx context.Context, cmdStr string, variables *vars.Variables) string { + return RunProcWithContextVars(ctx, cmdStr, variables).Result() } // Start starts the associated command as an OS process and does not wait for its result. -// Ensure proper access to the process' input/output (stdin,stdout,stderr) has been -// setup prior to calling p.Start(). -// Use p.Err() to access any error that may have occured during execution. +// This call should follow a process creation using NewProc. +// If you don't want to use the internal combined output streams, make sure to configure access +// to the process' input/output (stdin,stdout,stderr) prior to calling Proc.Start(). func (p *Proc) Start() *Proc { + if p.err != nil { + return p + } + if p.hasStarted() { return p } @@ -133,6 +180,34 @@ func (p *Proc) Start() *Proc { return p } + // wire an output if none was provided + if p.cmd.Stdout == nil { + p.cmd.Stdout = p.result + } + if p.cmd.Stderr == nil { + p.cmd.Stderr = p.result + } + + // apply user id and user grp + var procCred *syscall.Credential + if p.userid != nil { + procCred = &syscall.Credential{ + Uid: uint32(*p.userid), + } + } + if p.groupid != nil { + if procCred == nil { + procCred = new(syscall.Credential) + } + procCred.Uid = uint32(*p.groupid) + } + if procCred != nil { + if p.cmd.SysProcAttr == nil { + p.cmd.SysProcAttr = new(syscall.SysProcAttr) + } + p.cmd.SysProcAttr.Credential = procCred + } + if err := p.cmd.Start(); err != nil { p.err = err return p @@ -145,20 +220,76 @@ func (p *Proc) Start() *Proc { return p } +// SetVars sets session variables for Proc +func (p *Proc) SetVars(variables *vars.Variables) *Proc { + p.vars = variables + return p +} + // Command returns the os/exec.Cmd that started the process func (p *Proc) Command() *osexec.Cmd { return p.cmd } +// SetUserid looks up the user by a numerical id or +// by a name to be used for the process when launched. +func (p *Proc) SetUserid(user string) *Proc { + if p.err != nil { + return p + } + uid, err := lookupUserID(user) + if err != nil { + p.err = err + return p + } + + p.userid = &uid + + return p +} + +// SetGroupid looks up the group by a numerical id or +// by a name to be used for the process when launched. +func (p *Proc) SetGroupid(grp string) *Proc { + if p.err != nil { + return p + } + + gid, err := lookupGroupID(grp) + if err != nil { + p.err = err + return p + } + + p.groupid = &gid + + return p +} + +// SetWorkDir sets the working directory for the command +func (p *Proc) SetWorkDir(path string) *Proc { + p.cmd.Dir = path + return p +} + // Peek attempts to read process state information func (p *Proc) Peek() *Proc { p.state = p.cmd.ProcessState return p } -// Wait waits for a process to complete (in a separate goroutine). -// Ensure p.Start() has been called prior to calling p.Wait() +// Wait waits for a previously started process to complete. +// Wait should follow Proc.StartXXX() methods to ensure completion. func (p *Proc) Wait() *Proc { + if p.err != nil { + return p + } + + if !p.hasStarted() { + p.err = fmt.Errorf("process not started") + return p + } + if p.cmd == nil { p.err = fmt.Errorf("command is nill") return p @@ -170,10 +301,14 @@ func (p *Proc) Wait() *Proc { return p.Peek() } -// Run starts and wait for a process to complete. -// Before calling p.Run(), setup proper access to the process' input/output (i.e. stdin,stdout, stderr) +// Run starts and waits for a process to complete. func (p *Proc) Run() *Proc { - if p.Start().Err() != nil { + if p.err != nil { + return p + } + + if startErr := p.Start().Err(); startErr != nil { + p.err = startErr return p } return p.Wait() @@ -231,40 +366,37 @@ func (p *Proc) Err() error { // Kill halts the process func (p *Proc) Kill() *Proc { + if p.err != nil { + return p + } + if err := p.cmd.Process.Kill(); err != nil { p.err = err } return p } -// Out waits, after StartProc or Proc.Start has been called, for the cmd to complete -// and returns the combined result (Stdout and Stderr) as a single reader to be streamed. +// Out returns the combined result (Stdout/Stderr) as a single reader if StartProc, RunProc, or Run +// package function was used to initiate the process. If Stdout/Stderr was set independently +// (i.e. with proc.Setstdout(...)) proc.Out will be nil. +// +// NB: Out used to start/wait the process if necessary. However, that behavior has been deprecated. +// You must ensure the process has been properly initiated and wait for completion prior to calling Out. func (p *Proc) Out() io.Reader { - if !p.hasStarted() { - p.cmd.Stdout = p.result - p.cmd.Stderr = p.result - if err := p.Start().Err(); err != nil { - return strings.NewReader(fmt.Sprintf("proc: out failed: %s", err)) - } - } - - if !p.Exited() { - if err := p.Wait().Err(); err != nil { - return strings.NewReader(fmt.Sprintf("proc: out: failed to wait: %s", err)) - } - } - return p.result } -// Result waits, after proc.Start or proc.StartProc has been called, for the cmd to complete -// and returns the combined stdout and stderr result as a string value. +// Result returns the combined stdout and stderr (see Proc.Out()) result as a string value. +// If there was a previous error in the call chain, this will return the error as a string. func (p *Proc) Result() string { - p.Out() - if p.Err() != nil { - return p.Err().Error() + if p.result == nil { + return "result " } - return strings.TrimSpace(p.result.String()) + result := strings.TrimSpace(p.result.String()) + if err := p.Err(); err != nil && result == "" { + return err.Error() + } + return result } // Stdin returns the standard input stream for the process @@ -278,6 +410,7 @@ func (p *Proc) SetStdin(in io.Reader) { } // GetInputPipe returns a stream where the process input can be written to +// Deprecated: conflicts with the way the underlying exe.Command works func (p *Proc) GetInputPipe() io.Writer { return p.inputPipe } @@ -293,6 +426,7 @@ func (p *Proc) SetStdout(out io.Writer) { } // GetOutputPipe returns a stream where the process output can be read from +// Deprecated: conflicts with the way the underlying exe.Command works func (p *Proc) GetOutputPipe() io.Reader { return p.outputPipe } @@ -308,6 +442,7 @@ func (p *Proc) SetStderr(out io.Writer) { } // GetErrorPipe returns a stream where the process error can be read from +// Deprecated: conflicts with the way the underlying exe.Command works func (p *Proc) GetErrorPipe() io.Reader { return p.errorPipe } @@ -320,3 +455,47 @@ func (p *Proc) hasStarted() bool { func Parse(cmd string) ([]string, error) { return parse(cmd) } + +func lookupUserID(userid string) (int, error) { + var uid int + var usr *user.User + var err error + + // assume userid is a valid numerical user id + usr, err = user.LookupId(userid) + if err == nil { + uid, _ = strconv.Atoi(usr.Uid) + return uid, nil + } + + // if not numercal id, lookup by username + usr, err = user.Lookup(userid) + if err == nil { + uid, _ = strconv.Atoi(usr.Uid) + return uid, nil + } + + return 0, err +} + +func lookupGroupID(grpid string) (int, error) { + var gid int + var grp *user.Group + var err error + + // assume grpid is a valid numerical group id + grp, err = user.LookupGroupId(grpid) + if err == nil { + gid, _ = strconv.Atoi(grp.Gid) + return gid, nil + } + + // if not numercal id, lookup by groupname + grp, err = user.LookupGroup(grpid) + if err == nil { + gid, _ = strconv.Atoi(grp.Gid) + return gid, nil + } + + return 0, err +} diff --git a/vendor/github.com/vladimirvivien/gexe/filesys.go b/vendor/github.com/vladimirvivien/gexe/filesys.go index ca1cde7f..bc683925 100644 --- a/vendor/github.com/vladimirvivien/gexe/filesys.go +++ b/vendor/github.com/vladimirvivien/gexe/filesys.go @@ -1,6 +1,7 @@ package gexe import ( + "context" "os" "github.com/vladimirvivien/gexe/fs" @@ -31,23 +32,33 @@ func (e *Echo) PathInfo(path string) *fs.FSInfo { return fs.PathWithVars(path, e.vars).Info() } +// FileReadWithContext uses specified context to provide methods to read file +// content at path. +func (e *Echo) FileReadWithContext(ctx context.Context, path string) *fs.FileReader { + return fs.ReadWithContextVars(ctx, path, e.vars) +} + // FileRead provides methods to read file content -// -// FileRead(path).Lines() func (e *Echo) FileRead(path string) *fs.FileReader { - return fs.PathWithVars(path, e.vars).Read() + return fs.ReadWithContextVars(context.Background(), path, e.vars) +} + +// FileWriteWithContext uses context ctx to create a fs.FileWriter to write content to provided path +func (e *Echo) FileWriteWithContext(ctx context.Context, path string) *fs.FileWriter { + return fs.WriteWithContextVars(ctx, path, e.vars) } -// FileWrite provides methods to write content to provided path -// -// FileWrite(path).String("hello world") +// FileWrite creates a fs.FileWriter to write content to provided path func (e *Echo) FileWrite(path string) *fs.FileWriter { - return fs.PathWithVars(path, e.vars).Write() + return fs.WriteWithContextVars(context.Background(), path, e.vars) +} + +// FileAppend creates a new fs.FileWriter to append content to provided path +func (e *Echo) FileAppendWithContext(ctx context.Context, path string) *fs.FileWriter { + return fs.AppendWithContextVars(ctx, path, e.vars) } -// FileAppend provides methods to append content to provided path -// -// FileAppend(path).String("hello world") +// FileAppend creates a new fs.FileWriter to append content to provided path func (e *Echo) FileAppend(path string) *fs.FileWriter { - return fs.PathWithVars(path, e.vars).Append() + return fs.AppendWithContextVars(context.Background(), path, e.vars) } diff --git a/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go b/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go index 8e5be7f4..1e75785f 100644 --- a/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go +++ b/vendor/github.com/vladimirvivien/gexe/fs/file_reader.go @@ -3,6 +3,7 @@ package fs import ( "bufio" "bytes" + "context" "io" "os" @@ -10,29 +11,55 @@ import ( ) type FileReader struct { - err error - path string - info os.FileInfo - mode os.FileMode - vars *vars.Variables + err error + path string + info os.FileInfo + mode os.FileMode + vars *vars.Variables + content *bytes.Buffer + ctx context.Context } -// Read creates a new FileReader using the provided path. -// A non-nil FileReader.Err() is returned if file does not exist -// or another error is generated. -func Read(path string) *FileReader { - info, err := os.Stat(path) +// ReadWithContextVars uses specified context and session variables to read the file at path +// and returns a *FileReader to access its content +func ReadWithContextVars(ctx context.Context, path string, variables *vars.Variables) *FileReader { + if variables == nil { + variables = &vars.Variables{} + } + filePath := variables.Eval(path) + + if err := ctx.Err(); err != nil { + return &FileReader{err: err, path: filePath} + } + + info, err := os.Stat(filePath) if err != nil { - return &FileReader{err: err, path: path} + return &FileReader{err: err, path: filePath} + } + + fileData, err := os.ReadFile(filePath) + if err != nil { + return &FileReader{err: err, path: filePath} + } + + return &FileReader{ + path: filePath, + info: info, + mode: info.Mode(), + content: bytes.NewBuffer(fileData), + vars: variables, + ctx: ctx, } - return &FileReader{path: path, info: info, mode: info.Mode()} } -// ReadWithVars creates a new FileReader and sets the reader's session variables +// ReadWithVars uses session variables to create a new FileReader func ReadWithVars(path string, variables *vars.Variables) *FileReader { - reader := Read(variables.Eval(path)) - reader.vars = variables - return reader + return ReadWithContextVars(context.Background(), path, variables) +} + +// Read reads the file at path and returns FileReader to access its content +func Read(path string) *FileReader { + return ReadWithContextVars(context.Background(), path, &vars.Variables{}) } // SetVars sets the FileReader's session variables @@ -41,6 +68,12 @@ func (fr *FileReader) SetVars(variables *vars.Variables) *FileReader { return fr } +// SetContext sets the context for the FileReader operations +func (fr *FileReader) SetContext(ctx context.Context) *FileReader { + fr.ctx = ctx + return fr +} + // Err returns an operation error during file read. func (fr *FileReader) Err() error { return fr.err @@ -53,36 +86,32 @@ func (fr *FileReader) Info() os.FileInfo { // String returns the content of the file as a string value func (fr *FileReader) String() string { - file, err := os.Open(fr.path) - if err != nil { - fr.err = err - return "" - } - defer file.Close() - - buf := new(bytes.Buffer) - if _, err := buf.ReadFrom(file); err != nil { - fr.err = err - return "" - } - - return buf.String() + return fr.content.String() } // Lines returns the content of the file as slice of string func (fr *FileReader) Lines() []string { - file, err := os.Open(fr.path) - if err != nil { + if fr.err != nil { + return []string{} + } + + if err := fr.ctx.Err(); err != nil { fr.err = err return []string{} } + var lines []string - scnr := bufio.NewScanner(file) + scnr := bufio.NewScanner(fr.content) for scnr.Scan() { + if err := fr.ctx.Err(); err != nil { + fr.err = err + break + } lines = append(lines, scnr.Text()) } + // err should never happen, but capture it anyway if scnr.Err() != nil { fr.err = scnr.Err() return []string{} @@ -93,33 +122,31 @@ func (fr *FileReader) Lines() []string { // Bytes returns the content of the file as []byte func (fr *FileReader) Bytes() []byte { - file, err := os.Open(fr.path) - if err != nil { - fr.err = err + if fr.err != nil { return []byte{} } - defer file.Close() - buf := new(bytes.Buffer) - - if _, err := buf.ReadFrom(file); err != nil { + if err := fr.ctx.Err(); err != nil { fr.err = err return []byte{} } - return buf.Bytes() + return fr.content.Bytes() } // Into reads the content of the file and writes // it into the specified Writer func (fr *FileReader) Into(w io.Writer) *FileReader { - file, err := os.Open(fr.path) - if err != nil { + if fr.err != nil { + return fr + } + + if err := fr.ctx.Err(); err != nil { fr.err = err return fr } - defer file.Close() - if _, err := io.Copy(w, file); err != nil { + + if _, err := io.Copy(w, fr.content); err != nil { fr.err = err } return fr diff --git a/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go b/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go index 3360669d..06291007 100644 --- a/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go +++ b/vendor/github.com/vladimirvivien/gexe/fs/file_writer.go @@ -1,6 +1,7 @@ package fs import ( + "context" "io" "os" @@ -14,11 +15,27 @@ type FileWriter struct { mode os.FileMode flags int vars *vars.Variables + ctx context.Context } -// Write creates a new FileWriter with flags os.O_CREATE | os.O_TRUNC | os.O_WRONLY and mode 0644. -func Write(path string) *FileWriter { - fw := &FileWriter{path: path, flags: os.O_CREATE | os.O_TRUNC | os.O_WRONLY, mode: 0644, vars: &vars.Variables{}} +// WriteWithVars uses the specified context and session variables to create a new FileWriter +// that can be used to write content to file at path. The file will be created with: +// +// os.O_CREATE | os.O_TRUNC | os.O_WRONLY +func WriteWithContextVars(ctx context.Context, path string, variables *vars.Variables) *FileWriter { + if variables == nil { + variables = &vars.Variables{} + } + filePath := variables.Eval(path) + + fw := &FileWriter{ + path: filePath, + flags: os.O_CREATE | os.O_TRUNC | os.O_WRONLY, + mode: 0644, + vars: variables, + ctx: ctx, + } + info, err := os.Stat(fw.path) if err == nil { fw.finfo = info @@ -26,29 +43,54 @@ func Write(path string) *FileWriter { return fw } -// WriteWithVars creates a new FileWriter and sets sessions variables +// WriteWithVars uses sesison variables to create a new FileWriter to write content to file func WriteWithVars(path string, variables *vars.Variables) *FileWriter { - fw := Write(variables.Eval(path)) - fw.vars = variables - return fw + return WriteWithContextVars(context.Background(), path, variables) } -// Append creates a new FileWriter with flags os.O_CREATE | os.O_APPEND | os.O_WRONLY and mode 0644 -func Append(path string) *FileWriter { - fw := &FileWriter{path: path, flags: os.O_CREATE | os.O_APPEND | os.O_WRONLY, mode: 0644} - info, err := os.Stat(fw.path) - if err == nil { - fw.finfo = info +// Write creates a new FileWriter to write file content +func Write(path string) *FileWriter { + return WriteWithContextVars(context.Background(), path, &vars.Variables{}) +} + +// AppendWithContextVars uses the specified context and session variables to create a new FileWriter +// that can be used to append content existing file at path. The file will be open with: +// +// os.O_CREATE | os.O_APPEND | os.O_WRONLY +// +// and mode 0644 +func AppendWithContextVars(ctx context.Context, path string, variables *vars.Variables) *FileWriter { + if variables == nil { + variables = &vars.Variables{} + } + filePath := variables.Eval(path) + + fw := &FileWriter{ + path: filePath, + flags: os.O_CREATE | os.O_APPEND | os.O_WRONLY, + mode: 0644, + vars: variables, + ctx: ctx, } + info, err := os.Stat(fw.path) + if err != nil { + fw.err = err + return fw + } + fw.finfo = info return fw } -// AppendWithVars creates a new FileWriter and sets session variables +// AppendWithVars uses the specified session variables to create a FileWriter +// to write content to file at path. func AppendWitVars(path string, variables *vars.Variables) *FileWriter { - fw := Append(variables.Eval(path)) - fw.vars = variables - return fw + return AppendWithContextVars(context.Background(), path, variables) +} + +// Append creates FileWriter to write content to file at path +func Append(path string) *FileWriter { + return AppendWithContextVars(context.Background(), path, &vars.Variables{}) } // SetVars sets session variables for FileWriter @@ -64,6 +106,12 @@ func (fw *FileWriter) WithMode(mode os.FileMode) *FileWriter { return fw } +// WithContext sets an execution context for the FileWriter operations +func (fw *FileWriter) WithContext(ctx context.Context) *FileWriter { + fw.ctx = ctx + return fw +} + // Err returns FileWriter error during execution func (fw *FileWriter) Err() error { return fw.err @@ -77,6 +125,9 @@ func (fw *FileWriter) Info() os.FileInfo { // String writes the provided str into the file. Any // error that occurs can be accessed with FileWriter.Err(). func (fw *FileWriter) String(str string) *FileWriter { + if fw.err != nil { + return fw + } file, err := os.OpenFile(fw.path, fw.flags, fw.mode) if err != nil { fw.err = err @@ -96,6 +147,15 @@ func (fw *FileWriter) String(str string) *FileWriter { // Lines writes the slice of strings into the file. // Any error will be captured and returned via FileWriter.Err(). func (fw *FileWriter) Lines(lines []string) *FileWriter { + if fw.err != nil { + return fw + } + + if err := fw.ctx.Err(); err != nil { + fw.err = err + return fw + } + file, err := os.OpenFile(fw.path, fw.flags, fw.mode) if err != nil { fw.err = err @@ -108,6 +168,11 @@ func (fw *FileWriter) Lines(lines []string) *FileWriter { len := len(lines) for i, line := range lines { + if err := fw.ctx.Err(); err != nil { + fw.err = err + break + } + if _, err := file.WriteString(line); err != nil { fw.err = err return fw @@ -125,6 +190,15 @@ func (fw *FileWriter) Lines(lines []string) *FileWriter { // Bytes writes the []bytre provided into the file. // Any error can be accessed using FileWriter.Err(). func (fw *FileWriter) Bytes(data []byte) *FileWriter { + if fw.err != nil { + return fw + } + + if err := fw.ctx.Err(); err != nil { + fw.err = err + return fw + } + file, err := os.OpenFile(fw.path, fw.flags, fw.mode) if err != nil { fw.err = err @@ -145,6 +219,15 @@ func (fw *FileWriter) Bytes(data []byte) *FileWriter { // writes them to the file. Any error will be captured // and returned by fw.Err(). func (fw *FileWriter) From(r io.Reader) *FileWriter { + if fw.err != nil { + return fw + } + + if err := fw.ctx.Err(); err != nil { + fw.err = err + return fw + } + file, err := os.OpenFile(fw.path, fw.flags, fw.mode) if err != nil { fw.err = err diff --git a/vendor/github.com/vladimirvivien/gexe/functions.go b/vendor/github.com/vladimirvivien/gexe/functions.go index 22555a8b..a8616fa2 100644 --- a/vendor/github.com/vladimirvivien/gexe/functions.go +++ b/vendor/github.com/vladimirvivien/gexe/functions.go @@ -1,6 +1,7 @@ package gexe import ( + "context" "os" "github.com/vladimirvivien/gexe/exec" @@ -23,8 +24,8 @@ func Variables() *vars.Variables { // // Environment vars can be used in string values // using Eval("building for os=$GOOS") -func Envs(val string) *Echo { - return DefaultEcho.Envs(val) +func Envs(val ...string) *Echo { + return DefaultEcho.Envs(val...) } // SetEnv sets a process environment variable. @@ -32,18 +33,15 @@ func SetEnv(name, value string) *Echo { return DefaultEcho.SetEnv(name, value) } -// Vars declares session-scope variables using -// a multi-line space-separated list: -// -// Envs("foo=bar platform=amd64") +// Vars declares multiple session-scope variables using +// string literals: // -// Session vars can be used in string values -// using Eval("My foo=$foo"). +// Envs("foo=bar", "platform=amd64", `"data="info ${platform}"`) // // Note that session vars are only available // for the running process. -func Vars(val string) *Echo { - return DefaultEcho.Vars(val) +func Vars(variables ...string) *Echo { + return DefaultEcho.Vars(variables...) } // SetVar declares a session variable. @@ -63,10 +61,22 @@ func Eval(str string) string { return DefaultEcho.Eval(str) } +// NewProcWithContext setups a new process with specified context and command cmdStr and returns immediately +// without starting. Information about the running process is stored in *exec.Proc. +func NewProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return DefaultEcho.NewProcWithContext(ctx, cmdStr) +} + // NewProc setups a new process with specified command cmdStr and returns immediately // without starting. Information about the running process is stored in *exec.Proc. func NewProc(cmdStr string) *exec.Proc { - return DefaultEcho.NewProc(cmdStr) + return DefaultEcho.NewProcWithContext(context.Background(), cmdStr) +} + +// StartProcWith executes the command in cmdStr with the specified contex and returns immediately +// without waiting. Information about the running process is stored in *exec.Proc. +func StartProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return DefaultEcho.StartProcWithContext(ctx, cmdStr) } // StartProc executes the command in cmdStr and returns immediately @@ -75,12 +85,24 @@ func StartProc(cmdStr string) *exec.Proc { return DefaultEcho.StartProc(cmdStr) } +// RunProcWithContext executes command in cmdStr, with specified ctx, and waits for the result. +// It returns a *Proc with information about the executed process. +func RunProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return DefaultEcho.RunProc(cmdStr) +} + // RunProc executes command in cmdStr and waits for the result. // It returns a *Proc with information about the executed process. func RunProc(cmdStr string) *exec.Proc { return DefaultEcho.RunProc(cmdStr) } +// RunWithContext executes cmdStr, with specified context, and waits for completion. +// It returns the result as a string. +func RunWithContext(ctx context.Context, cmdStr string) string { + return DefaultEcho.RunWithContext(ctx, cmdStr) +} + // Run executes cmdStr, waits, and returns the result as a string. func Run(cmdStr string) string { return DefaultEcho.Run(cmdStr) @@ -152,24 +174,54 @@ func RmPath(path string) *fs.FSInfo { return DefaultEcho.RmPath(path) } +// FileRead uses context ctx to read file content from path +func FileReadWithContext(ctx context.Context, path string) *fs.FileReader { + return DefaultEcho.FileReadWithContext(ctx, path) +} + // FileRead provides methods to read file content from path func FileRead(path string) *fs.FileReader { - return DefaultEcho.FileRead(path) + return DefaultEcho.FileReadWithContext(context.Background(), path) +} + +// FileWriteWithContext uses context ctx to write file content to path +func FileWriteWithContext(ctx context.Context, path string) *fs.FileWriter { + return DefaultEcho.FileWriteWithContext(ctx, path) } // FileWrite provides methods to write file content to path func FileWrite(path string) *fs.FileWriter { - return DefaultEcho.FileWrite(path) + return DefaultEcho.FileWriteWithContext(context.Background(), path) +} + +// HttpGetWithContext uses context ctx to start an HTTP GET operation to retrieve resource at URL/path +func HttpGetWithContext(ctx context.Context, url string, paths ...string) *http.ResourceReader { + return DefaultEcho.HttpGetWithContext(ctx, url, paths...) +} + +// HttpGet starts an HTTP GET operation to retrieve resource at URL/path +func HttpGet(url string, paths ...string) *http.ResourceReader { + return DefaultEcho.HttpGetWithContext(context.Background(), url, paths...) +} + +// Get is a convenient alias for HttpGet that retrieves specified resource at given URL/path +func Get(url string, paths ...string) *http.Response { + return DefaultEcho.Get(url, paths...) +} + +// HttpPostWithContext uses context ctx to start an HTTP POST operation to post resource to URL/path +func HttpPostWithContext(ctx context.Context, url string, paths ...string) *http.ResourceWriter { + return DefaultEcho.HttpPostWithContext(ctx, url, paths...) } -// GetUrl creates a *http.ResourceReader to retrieve HTTP content -func GetUrl(url string) *http.ResourceReader { - return DefaultEcho.Get(url) +// HttpPost starts an HTTP POST operation to post resource to URL/path +func HttpPost(url string, paths ...string) *http.ResourceWriter { + return DefaultEcho.HttpPostWithContext(context.Background(), url, paths...) } -// PostUrl creates a *http.ResourceWriter to write content to an HTTP server -func PostUrl(url string) *http.ResourceWriter { - return DefaultEcho.Post(url) +// Post is a convenient alias for HttpPost to post data at specified URL +func Post(data []byte, url string) *http.Response { + return DefaultEcho.Post(data, url) } // Prog returns program information via *prog.Info diff --git a/vendor/github.com/vladimirvivien/gexe/http.go b/vendor/github.com/vladimirvivien/gexe/http.go index 95c657cb..71c6e0b4 100644 --- a/vendor/github.com/vladimirvivien/gexe/http.go +++ b/vendor/github.com/vladimirvivien/gexe/http.go @@ -1,27 +1,48 @@ package gexe import ( + "context" "strings" "github.com/vladimirvivien/gexe/http" ) -// Get creates a *http.ResourceReader to read resource content from HTTP server -func (e *Echo) Get(url string, paths ...string) *http.ResourceReader { +// HttpGetWithContext uses context ctx to start an HTTP GET operation to retrieve server resource from given URL/paths. +func (e *Echo) HttpGetWithContext(ctx context.Context, url string, paths ...string) *http.ResourceReader { var exapandedUrl strings.Builder exapandedUrl.WriteString(e.vars.Eval(url)) for _, path := range paths { exapandedUrl.WriteString(e.vars.Eval(path)) } - return http.GetWithVars(exapandedUrl.String(), e.vars) + return http.GetWithContextVars(ctx, exapandedUrl.String(), e.vars) } -// Post creates a *http.ResourceWriter to write content to an HTTP server -func (e *Echo) Post(url string, paths ...string) *http.ResourceWriter { +// HttpGetWithContext starts an HTTP GET operation to retrieve server resource from given URL/paths. +func (e *Echo) HttpGet(url string, paths ...string) *http.ResourceReader { + return e.HttpGetWithContext(context.Background(), url, paths...) +} + +// HttpPostWithContext uses context ctx to start an HTTP POST operation to post resource to a server at given URL/path. +func (e *Echo) HttpPostWithContext(ctx context.Context, url string, paths ...string) *http.ResourceWriter { var exapandedUrl strings.Builder exapandedUrl.WriteString(e.vars.Eval(url)) for _, path := range paths { exapandedUrl.WriteString(e.vars.Eval(path)) } - return http.PostWithVars(exapandedUrl.String(), e.vars) + return http.PostWithContextVars(ctx, exapandedUrl.String(), e.vars) +} + +// HttpPost starts an HTTP POST operation to post resource to a server at given URL/path. +func (e *Echo) HttpPost(url string, paths ...string) *http.ResourceWriter { + return e.HttpPostWithContext(context.Background(), url, paths...) +} + +// Get is convenient alias for HttpGet to retrieve a resource at given URL/path +func (e *Echo) Get(url string, paths ...string) *http.Response { + return e.HttpGet(url, paths...).Do() +} + +// Post is a convenient alias for HttpPost to post the specified data to given URL/path +func (e *Echo) Post(data []byte, url string, paths ...string) *http.Response { + return e.HttpPost(url, paths...).Bytes(data).Do() } diff --git a/vendor/github.com/vladimirvivien/gexe/http/http_reader.go b/vendor/github.com/vladimirvivien/gexe/http/http_reader.go index 60db91d0..1dc380f6 100644 --- a/vendor/github.com/vladimirvivien/gexe/http/http_reader.go +++ b/vendor/github.com/vladimirvivien/gexe/http/http_reader.go @@ -1,8 +1,11 @@ package http import ( + "bytes" + "context" "io" "net/http" + "strings" "time" "github.com/vladimirvivien/gexe/vars" @@ -10,22 +13,38 @@ import ( // ResourceReader provides types and methods to read content of resources from a server using HTTP type ResourceReader struct { - client *http.Client - err error - url string - vars *vars.Variables + client *http.Client + err error + url string + vars *vars.Variables + ctx context.Context + data io.Reader + headers http.Header } -// Get initiates a "GET" operation for the specified resource -func Get(url string) *ResourceReader { - return &ResourceReader{url: url, client: &http.Client{}, vars: &vars.Variables{}} +// GetWithContextVars uses context ctx and session variables to initiate +// a "GET" operation for the specified resource +func GetWithContextVars(ctx context.Context, url string, variables *vars.Variables) *ResourceReader { + if variables == nil { + variables = &vars.Variables{} + } + + return &ResourceReader{ + ctx: ctx, + url: variables.Eval(url), + client: &http.Client{}, + vars: &vars.Variables{}, + } } -// Get initiates a "GET" operation and sets session variables +// GetWithVars uses session vars to initiate a "GET" operation func GetWithVars(url string, variables *vars.Variables) *ResourceReader { - r := Get(variables.Eval(url)) - r.vars = variables - return r + return GetWithContextVars(context.Background(), url, variables) +} + +// Get initiates a "GET" operation for the specified resource +func Get(url string) *ResourceReader { + return GetWithContextVars(context.Background(), url, &vars.Variables{}) } // SetVars sets session variables for ResourceReader @@ -45,46 +64,60 @@ func (r *ResourceReader) WithTimeout(to time.Duration) *ResourceReader { return r } -// Do invokes the client.Get to "GET" the content from server -// Use Response.Err() to access server response errors -func (r *ResourceReader) Do() *Response { - res, err := r.client.Get(r.url) - if err != nil { - return &Response{err: err} - } - return &Response{stat: res.Status, statCode: res.StatusCode, body: res.Body} +// WithContext sets the context for the HTTP request +func (r *ResourceReader) WithContext(ctx context.Context) *ResourceReader { + r.ctx = ctx + return r } -// Bytes returns the server response as a []byte -// This is a shorthad for ResourceReader.Do().Bytes() -func (r *ResourceReader) Bytes() []byte { - resp := r.Do() - if resp.Err() != nil { - r.err = resp.Err() - return nil - } - return resp.Bytes() +// WithHeaders sets all HTTP headers for GET request +func (r *ResourceReader) WithHeaders(h http.Header) *ResourceReader { + r.headers = h + return r } -// String returns the server response as a string. -// It is a shorthad for ResourceReader.Do().String() -func (r *ResourceReader) String() string { - resp := r.Do() - if resp.Err() != nil { - r.err = resp.Err() - return "" - } - return resp.String() +// AddHeader convenience method to add request header +func (r *ResourceReader) AddHeader(key, value string) *ResourceReader { + r.headers.Add(r.vars.Eval(key), r.vars.Eval(value)) + return r } -// Body returns the server response body (as io.ReadCloser). -// It is a shorthand for ResourceReader().Do().Body() -// NOTE: ensure to close the stream when finished. -func (r *ResourceReader) Body() io.ReadCloser { - resp := r.Do() - if resp.Err() != nil { - r.err = resp.Err() - return nil +// SetHeader convenience method to set a specific header +func (r *ResourceReader) SetHeader(key, value string) *ResourceReader { + r.headers.Set(r.vars.Eval(key), r.vars.Eval(value)) + return r +} + +// RequestString sets GET request data as string +func (r *ResourceReader) String(val string) *ResourceReader { + r.data = strings.NewReader(r.vars.Eval(val)) + return r +} + +// RequestBytes sets GET request data as byte slice +func (r *ResourceReader) Bytes(data []byte) *ResourceReader { + r.data = bytes.NewReader(data) + return r +} + +// RequestBody sets GET request content as io.Reader +func (r *ResourceReader) Body(body io.Reader) *ResourceReader { + r.data = body + return r +} + +// Do is a terminal method that actually retrieves the HTTP resource from the server. +// It returns a gexe/http/*Response instance that can be used to access the result. +func (r *ResourceReader) Do() *Response { + req, err := http.NewRequestWithContext(r.ctx, "GET", r.url, r.data) + if err != nil { + return &Response{err: err} + } + + res, err := r.client.Do(req) + if err != nil { + return &Response{err: err} } - return resp.Body() + + return &Response{stat: res.Status, statCode: res.StatusCode, body: res.Body} } diff --git a/vendor/github.com/vladimirvivien/gexe/http/http_writer.go b/vendor/github.com/vladimirvivien/gexe/http/http_writer.go index 3aa91637..fa077f90 100644 --- a/vendor/github.com/vladimirvivien/gexe/http/http_writer.go +++ b/vendor/github.com/vladimirvivien/gexe/http/http_writer.go @@ -2,6 +2,7 @@ package http import ( "bytes" + "context" "io" "net/http" "net/url" @@ -11,105 +12,101 @@ import ( "github.com/vladimirvivien/gexe/vars" ) -// ResourceWriter represents types and methods used to post resource data to an HTTP server +// ResourceWriter provides types and methods used to post resource data to an HTTP server type ResourceWriter struct { client *http.Client err error url string headers http.Header data io.Reader - res *Response vars *vars.Variables + ctx context.Context } -// Post starts a "POST" HTTP operation to the provided resource. -func Post(resource string) *ResourceWriter { - return &ResourceWriter{url: resource, client: &http.Client{}, headers: make(http.Header), vars: &vars.Variables{}} +// PostWithContextVars uses the specified context ctx and session variable to +// start an HTTP "POST" operation +func PostWithContextVars(ctx context.Context, resource string, variables *vars.Variables) *ResourceWriter { + if variables == nil { + variables = &vars.Variables{} + } + + return &ResourceWriter{ + ctx: ctx, + url: variables.Eval(resource), + client: &http.Client{}, + headers: make(http.Header), + vars: variables, + } } -// PostWithVars sets up a "POST" operation and sets its session variables +// PostWithVars uses session variables to start an HTTP "POST" operation func PostWithVars(resource string, variables *vars.Variables) *ResourceWriter { - w := Post(variables.Eval(resource)) - w.vars = variables - return w + return PostWithContextVars(context.Background(), resource, variables) } -// SetVars sets session variables for the ResourceWriter +// Post starts an HTTP "POST" operation using the provided URL. +func Post(resource string) *ResourceWriter { + return PostWithContextVars(context.Background(), resource, &vars.Variables{}) +} + +// SetVars sets gexe session variables to be used by the post operation func (w *ResourceWriter) SetVars(variables *vars.Variables) *ResourceWriter { w.vars = variables return w } -// WithTimeout sets the HTTP client's timeout +// WithTimeout sets the HTTP client's timeout used for the post operation func (w *ResourceWriter) WithTimeout(to time.Duration) *ResourceWriter { w.client.Timeout = to return w } -// Err returns the last known error for the post operation -func (w *ResourceWriter) Err() error { - return w.err +// WithContext sets the context to be used for the HTTP request +func (w *ResourceWriter) WithContext(ctx context.Context) *ResourceWriter { + w.ctx = ctx + return w } -// Do is a terminal method that completes the post request of data to the HTTP server. -func (w *ResourceWriter) Do() *ResourceWriter { - req, err := http.NewRequest("POST", w.url, w.data) - if err != nil { - w.err = err - w.res = &Response{} - return w - } - - // set headers - req.Header = w.headers - - // post request - res, err := w.client.Do(req) - if err != nil { - w.err = err - w.res = &Response{} - return w - } - - w.res = &Response{stat: res.Status, statCode: res.StatusCode, body: res.Body} - - return w +// Err returns the last error generated by the ResourceWriter +func (w *ResourceWriter) Err() error { + return w.err } -// WithHeaders sets all headers for the post operation +// WithHeaders sets all HTTP headers to be used by the post operation func (w *ResourceWriter) WithHeaders(h http.Header) *ResourceWriter { w.headers = h return w } -// AddHeader is a convenience method to add a single header +// AddHeader is a convenience method to add a single header for the post operation func (w *ResourceWriter) AddHeader(key, value string) *ResourceWriter { w.headers.Add(w.vars.Eval(key), w.vars.Eval(value)) return w } -// SetHeader is a convenience method to sets a specific header +// SetHeader is a convenience method to sets a specific header for the post operation func (w *ResourceWriter) SetHeader(key, value string) *ResourceWriter { w.headers.Set(w.vars.Eval(key), w.vars.Eval(value)) return w } -// String posts the string value as content to the server +// String posts the string value content to the server +// and returns a gexe/http/*Response func (w *ResourceWriter) String(val string) *ResourceWriter { w.data = strings.NewReader(w.vars.Eval(val)) - return w.Do() + return w } // Bytes posts the slice of bytes as content to the server func (w *ResourceWriter) Bytes(val []byte) *ResourceWriter { w.data = bytes.NewReader(val) - return w.Do() + return w } // Body provides an io reader to stream content to the server func (w *ResourceWriter) Body(val io.Reader) *ResourceWriter { w.data = val - return w.Do() + return w } // FormData posts form-encoded data as content to the server @@ -117,5 +114,25 @@ func (w *ResourceWriter) FormData(val map[string][]string) *ResourceWriter { w.SetHeader("Content-Type", "application/x-www-form-urlencoded") formData := url.Values(val) w.data = strings.NewReader(formData.Encode()) - return w.Do() + return w +} + +// Do is a terminal method that actually posts the HTTP request to the server. +// It returns a gexe/http/*Response instance that can be used to access post result. +func (w *ResourceWriter) Do() *Response { + req, err := http.NewRequestWithContext(w.ctx, "POST", w.url, w.data) + if err != nil { + return &Response{err: err} + } + + // set headers + req.Header = w.headers + + // post request + res, err := w.client.Do(req) + if err != nil { + return &Response{err: err} + } + + return &Response{stat: res.Status, statCode: res.StatusCode, body: res.Body} } diff --git a/vendor/github.com/vladimirvivien/gexe/procs.go b/vendor/github.com/vladimirvivien/gexe/procs.go index ed230675..34f6fcf5 100644 --- a/vendor/github.com/vladimirvivien/gexe/procs.go +++ b/vendor/github.com/vladimirvivien/gexe/procs.go @@ -1,6 +1,7 @@ package gexe import ( + "context" "fmt" "github.com/vladimirvivien/gexe/exec" @@ -9,26 +10,49 @@ import ( // NewProc setups a new process with specified command cmdStr and returns immediately // without starting. Use Proc.Wait to wait for exection and then retrieve process result. // Information about the running process is stored in *exec.Proc. +func (e *Echo) NewProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return exec.NewProcWithContextVars(ctx, cmdStr, e.vars) +} + +// NewProc a convenient function that calls NewProcWithContext with a default contet. func (e *Echo) NewProc(cmdStr string) *exec.Proc { - return exec.NewProcWithVars(cmdStr, e.vars) + return exec.NewProcWithContextVars(context.Background(), cmdStr, e.vars) +} + +// StartProc executes the command in cmdStr, with the specified context, and returns immediately +// without waiting. Use Proc.Wait to wait for exection and then retrieve process result. +// Information about the running process is stored in *Proc. +func (e *Echo) StartProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return exec.StartProcWithContextVars(ctx, cmdStr, e.vars) } // StartProc executes the command in cmdStr and returns immediately // without waiting. Use Proc.Wait to wait for exection and then retrieve process result. // Information about the running process is stored in *Proc. func (e *Echo) StartProc(cmdStr string) *exec.Proc { - return exec.StartProcWithVars(cmdStr, e.vars) + return exec.StartProcWithContextVars(context.Background(), cmdStr, e.vars) +} + +// RunProcWithContext executes command in cmdStr, with given context, and waits for the result. +// It returns a *Proc with information about the executed process. +func (e *Echo) RunProcWithContext(ctx context.Context, cmdStr string) *exec.Proc { + return exec.RunProcWithContextVars(ctx, cmdStr, e.vars) } // RunProc executes command in cmdStr and waits for the result. // It returns a *Proc with information about the executed process. func (e *Echo) RunProc(cmdStr string) *exec.Proc { - return exec.RunProcWithVars(cmdStr, e.vars) + return exec.RunProcWithContextVars(context.Background(), cmdStr, e.vars) +} + +// Run executes cmdStr, with given context, and returns the result as a string. +func (e *Echo) RunWithContext(ctx context.Context, cmdStr string) string { + return exec.RunWithContextVars(ctx, cmdStr, e.vars) } // Run executes cmdStr, waits, and returns the result as a string. func (e *Echo) Run(cmdStr string) string { - return exec.RunWithVars(cmdStr, e.vars) + return exec.RunWithContextVars(context.Background(), cmdStr, e.vars) } // Runout executes command cmdStr and prints out the result @@ -36,38 +60,72 @@ func (e *Echo) Runout(cmdStr string) { fmt.Print(e.Run(cmdStr)) } +// Commands creates a *exe.CommandBuilder, with the specified context, to build a multi-command execution flow. +func (e *Echo) CommandsWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandBuilder { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...) +} + // Commands returns a *exe.CommandBuilder to build a multi-command execution flow. func (e *Echo) Commands(cmdStrs ...string) *exec.CommandBuilder { - return exec.CommandsWithVars(e.vars, cmdStrs...) + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...) +} + +// StartAllWithContext uses the specified ctx to start sequential execution of each command, in cmdStrs, and does not +// wait for their completion. +func (e *Echo) StartAllWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Start() } // StartAll starts the sequential execution of each command, in cmdStrs, and does not // wait for their completion. func (e *Echo) StartAll(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Start() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Start() +} + +// RunAllWithContext executes each command sequentially, in cmdStrs, and wait for their completion. +func (e *Echo) RunAllWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Run() } // RunAll executes each command sequentially, in cmdStrs, and wait for their completion. func (e *Echo) RunAll(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Run() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Run() +} + +// StartConcurWithContext uses specified context to start the concurrent execution of each command, in cmdStrs, and does not +// wait for their completion. +func (e *Echo) StartConcurWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Concurr() } // StartConcur starts the concurrent execution of each command, in cmdStrs, and does not // wait for their completion. func (e *Echo) StartConcur(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Concurr() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Concurr() +} + +// RunConcurWithContext uses context to execute each command concurrently, in cmdStrs, and waits +// their completion. +func (e *Echo) RunConcurWithContext(ctx context.Context, cmdStrs ...string) *exec.CommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Concurr().Wait() } // RunConcur executes each command concurrently, in cmdStrs, and waits // their completion. func (e *Echo) RunConcur(cmdStrs ...string) *exec.CommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Concurr().Wait() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Concurr().Wait() +} + +// Pipe uses specified context to execute each command, in cmdStrs, by piping the result +// of the previous command as input to the next command until done. +func (e *Echo) PipeWithContext(ctx context.Context, cmdStrs ...string) *exec.PipedCommandResult { + return exec.CommandsWithContextVars(ctx, e.vars, cmdStrs...).Pipe() } // Pipe executes each command, in cmdStrs, by piping the result // of the previous command as input to the next command until done. func (e *Echo) Pipe(cmdStrs ...string) *exec.PipedCommandResult { - return exec.CommandsWithVars(e.vars, cmdStrs...).Pipe() + return exec.CommandsWithContextVars(context.Background(), e.vars, cmdStrs...).Pipe() } // ParseCommand parses the string into individual command tokens diff --git a/vendor/github.com/vladimirvivien/gexe/variables.go b/vendor/github.com/vladimirvivien/gexe/variables.go index 657d6d58..03aa304b 100644 --- a/vendor/github.com/vladimirvivien/gexe/variables.go +++ b/vendor/github.com/vladimirvivien/gexe/variables.go @@ -12,12 +12,12 @@ func (e *Echo) Variables() *vars.Variables { // Envs declares environment variables using // a multi-line space-separated list: // -// Envs("GOOS=linux GOARCH=amd64") +// Envs("GOOS=linux" "GOARCH=amd64", `platform="$GOOS:$GOARCH"`) // // Environment vars can be used in string values // using Eval("building for os=$GOOS") -func (e *Echo) Envs(val string) *Echo { - vars := e.vars.Envs(val) +func (e *Echo) Envs(variables ...string) *Echo { + vars := e.vars.Envs(variables...) e.err = vars.Err() return e } @@ -29,18 +29,15 @@ func (e *Echo) SetEnv(name, value string) *Echo { return e } -// Vars declares session-scope variables using -// a multi-line space-separated list: -// -// Envs("foo=bar platform=amd64") +// Vars declares multiple session-scope variables using +// string literal format: // -// Session vars can be used in string values -// using Eval("My foo=$foo"). +// Envs("foo=bar", "platform=amd64", `"data="info ${platform}"`) // // Note that session vars are only available // for the running process. -func (e *Echo) Vars(val string) *Echo { - vars := e.vars.Vars(val) +func (e *Echo) Vars(variables ...string) *Echo { + vars := e.vars.Vars(variables...) e.err = vars.Err() return e } diff --git a/vendor/github.com/vladimirvivien/gexe/vars/variables.go b/vendor/github.com/vladimirvivien/gexe/vars/variables.go index 4d32de8c..c42e3e6d 100644 --- a/vendor/github.com/vladimirvivien/gexe/vars/variables.go +++ b/vendor/github.com/vladimirvivien/gexe/vars/variables.go @@ -1,19 +1,23 @@ package vars import ( - "bufio" "os" "regexp" - "strings" "sync" ) var ( - varsKeyValRgx = regexp.MustCompile(`\s*=\s*`) - // varsLineRegx matches vars/envs of form "a =b c= $d e=f g=${h}" - varsLineRegx = regexp.MustCompile(`\w+\s*=\s*(\$?\{?)\w+(\}?)`) + // varsRegex matches variable pairs of the forms: a=b, c="${d}", e='f' g="h i j ${k}" + // Var pairs must not contain space between name and value. + varsRegex = regexp.MustCompile(`([A-Za-z0-9_]+)=["']?([^"']*?\$\{[A-Za-z0-9_\.\s]+\}[^"']*?|[^"']*)["']?`) ) +// varmap is used to store parsed variables +type varmap struct { + key string + value string +} + // Variables stores a variable map that used for variable expansion // in parsed commands and other parsed strings. type Variables struct { @@ -39,63 +43,64 @@ func (v *Variables) Err() error { return v.err } -// Envs declares process environment variables using -// a multi-line space-separated list of KEY=VAL format: -// i.e. GOOS=linux GOARCH=amd64 -func (v *Variables) Envs(val string) *Variables { - vars, err := v.parseVars(val) - if err != nil { - v.err = err +// Envs declares process environment variables with support for +// variable expansion. Each variable must use the form: +// +// = +// +// With no space between key name, equal sign, and value, +// i.e. Envs(`GOOS=linux`, `GOARCH=amd64`, `INFO="OS: ${GOOS}, ARC: ${GOARCH}"`) +func (v *Variables) Envs(variables ...string) *Variables { + if len(variables) == 0 { return v } - for key, value := range vars { - if err := os.Setenv(key, v.ExpandVar(value, v.Val)); err != nil { - v.err = err - return v - } + varmaps := v.parseVars(variables...) + for _, parsedVar := range varmaps { + v.SetEnv(parsedVar.key, parsedVar.value) } return v } -// SetEnv sets a process environment variable. -func (v *Variables) SetEnv(name, value string) *Variables { - if err := os.Setenv(name, v.ExpandVar(value, v.Val)); err != nil { +// SetEnv sets an environment variable key with value. +func (v *Variables) SetEnv(key, value string) *Variables { + if err := os.Setenv(key, v.ExpandVar(value, v.Val)); err != nil { v.err = err return v } return v } -// Vars declares an internal variable used during current gexe session. -// It uses a multi-line, space-separated list of KEY=VAL format: -// i.e. foo=bar fuzz=buzz -func (v *Variables) Vars(val string) *Variables { - vars, err := v.parseVars(val) - - if err != nil { - v.err = err +// Vars declares gexe session variables with support for +// variable expansion. Each variable must use the form: +// +// = +// +// With no space between key name, equal sign, and value, +// i.e. Vars(`foo=bar`, `fuzz=${foo}`, `dazz="this is ${fuzz}"`) +func (v *Variables) Vars(variables ...string) *Variables { + if len(variables) == 0 { return v } - // copy them - v.Lock() - defer v.Unlock() - for key, val := range vars { - v.vars[key] = val - } + varmaps := v.parseVars(variables...) + // set variables + for _, parsedVar := range varmaps { + v.SetVar(parsedVar.key, parsedVar.value) + } return v } -// SetVar declares an in-process local variable. +// SetVar declares a gexe session variable. func (v *Variables) SetVar(name, value string) *Variables { + expVar := v.ExpandVar(value, v.Val) v.Lock() defer v.Unlock() - v.vars[name] = v.ExpandVar(value, v.Val) + v.vars[name] = expVar return v } -// UnsetVar removes a previously set local variable. +// UnsetVar removes a previously set gexe session variable. func (v *Variables) UnsetVar(name string) *Variables { v.Lock() defer v.Unlock() @@ -103,46 +108,39 @@ func (v *Variables) UnsetVar(name string) *Variables { return v } -// Val searches for a Var with provided key, if not found -// searches for environment var, for running process, with same key -func (v *Variables) Val(name string) string { - //v.Lock() - //defer v.Unlock() - if val, ok := v.vars[name]; ok { +// Val searches for a gexe session variable with provided key, if not found +// searches for an environment variable with that key. +func (v *Variables) Val(key string) string { + v.RLock() + defer v.RUnlock() + if val, ok := v.vars[key]; ok { return val } - return os.Getenv(name) + return os.Getenv(key) } // Eval returns the string str with its content expanded -// with variable values i.e. Eval("I am $HOME") returns +// with variable references i.e. Eval("I am $HOME") returns // "I am " func (v *Variables) Eval(str string) string { return v.ExpandVar(str, v.Val) } -// parseVars parses multi-line, space-separated key=value pairs -// into map[string]string -func (v *Variables) parseVars(lines string) (map[string]string, error) { - // parse lines into envs = []{"KEY0=VAL0", "KEY1=VAL1",...} - var envs []string - scnr := bufio.NewScanner(strings.NewReader(lines)) - - for scnr.Scan() { - envs = append(envs, varsLineRegx.FindAllString(scnr.Text(), -1)...) - } - if err := scnr.Err(); err != nil { - return nil, err +// parseVars parses each var line and maps each key to value into []varmap result. +// This method does not do variable expansion. +func (v *Variables) parseVars(lines ...string) []varmap { + var result []varmap + if len(lines) == 0 { + return []varmap{} } - // parse each item in []string{"key=value",...} item into key=value - result := make(map[string]string) - for _, env := range envs { - kv := varsKeyValRgx.Split(env, 2) - if len(kv) == 2 { - result[kv[0]] = v.Eval(kv[1]) + // each line should contain ()=() pair + // matched with expressino which returns match[1] (key) and match[2] (value) + for _, line := range lines { + matches := varsRegex.FindStringSubmatch(line) + if len(matches) >= 3 { + result = append(result, varmap{key: matches[1], value: matches[2]}) } } - - return result, nil + return result } diff --git a/vendor/modules.txt b/vendor/modules.txt index 802c1521..6b390beb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -201,8 +201,8 @@ github.com/spf13/pflag # github.com/stretchr/testify v1.9.0 ## explicit; go 1.17 github.com/stretchr/testify/assert -# github.com/vladimirvivien/gexe v0.3.0 -## explicit; go 1.22 +# github.com/vladimirvivien/gexe v0.4.1 +## explicit; go 1.23 github.com/vladimirvivien/gexe github.com/vladimirvivien/gexe/exec github.com/vladimirvivien/gexe/fs