Skip to content

Commit

Permalink
fixup cmd.run
Browse files Browse the repository at this point in the history
  • Loading branch information
taigrr committed Nov 7, 2023
1 parent bd012b4 commit f2b2b3a
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 41 deletions.
105 changes: 72 additions & 33 deletions ingredients/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,114 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"

nats "github.com/nats-io/nats.go"

"github.com/gogrlx/grlx/ingredients"
"github.com/gogrlx/grlx/types"
)

var ec *nats.EncodedConn

func init() {
baseCMD := Cmd{}
ingredients.RegisterAllMethods(baseCMD)
}
var ErrCmdMethodUndefined = fmt.Errorf("cmd method undefined")

type Cmd struct {
ID string
Method string
Name string
RunAs string
Env []string
Path string
WorkingDir string
Async bool
id string
method string
params map[string]interface{}
}

func RegisterEC(n *nats.EncodedConn) {
ec = n
// TODO parse out the map here
func (c Cmd) Parse(id, method string, params map[string]interface{}) (types.RecipeCooker, error) {
if params == nil {
params = map[string]interface{}{}
}
return Cmd{
id: id, method: method,
params: params,
}, nil
}

func New(id, method string, params map[string]interface{}) Cmd {
return Cmd{ID: id, Method: method}
func (c Cmd) validate() error {
set, err := c.PropertiesForMethod(c.method)
if err != nil {
return err
}
propSet, err := ingredients.PropMapToPropSet(set)
if err != nil {
return err
}
for _, v := range propSet {
if v.IsReq {
if v.Key == "name" {
name, ok := c.params[v.Key].(string)
if !ok {
return types.ErrMissingName
}
if name == "" {
return types.ErrMissingName
}

} else {
if _, ok := c.params[v.Key]; !ok {
return fmt.Errorf("missing required property %s", v.Key)
}
}
}
}
return nil
}

func (c Cmd) Test(ctx context.Context) (types.Result, error) {
return types.Result{}, nil
return types.Result{
Succeeded: true,
Failed: false,
Changed: false,
Notes: []fmt.Stringer{types.SimpleNote("cmd would have been executed")},
}, nil
}

func (c Cmd) Apply(ctx context.Context) (types.Result, error) {
switch c.Method {
switch c.method {
case "run":
fallthrough
default:
// TODO define error type
return types.Result{Succeeded: false, Failed: true, Changed: false, Notes: nil}, fmt.Errorf("method %s undefined", c.Method)
return types.Result{Succeeded: false, Failed: true, Changed: false, Notes: nil},
errors.Join(ErrCmdMethodUndefined, fmt.Errorf("method %s undefined", c.method))

}
}

func (c Cmd) Methods() (string, []string) {
return "cmd", []string{"run"}
}

// TODO create map for method: type
func (c Cmd) PropertiesForMethod(method string) (map[string]string, error) {
return nil, nil
switch method {
case "run":
return ingredients.MethodPropsSet{
ingredients.MethodProps{Key: "name", Type: "string", IsReq: true},
ingredients.MethodProps{Key: "args", Type: "string", IsReq: false},
ingredients.MethodProps{Key: "env", Type: "[]string", IsReq: false},
ingredients.MethodProps{Key: "cwd", Type: "string", IsReq: false},
ingredients.MethodProps{Key: "runas", Type: "string", IsReq: false},
ingredients.MethodProps{Key: "path", Type: "string", IsReq: false},
ingredients.MethodProps{Key: "timeout", Type: "string", IsReq: false},
}.ToMap(), nil
default:
return nil, fmt.Errorf("method %s undefined", method)
}
}

// TODO parse out the map here
func (c Cmd) Parse(id, method string, params map[string]interface{}) (types.RecipeCooker, error) {
return New(id, method, params), nil
func (c Cmd) Methods() (string, []string) {
return "cmd", []string{"run"}
}

func (c Cmd) Properties() (map[string]interface{}, error) {
m := map[string]interface{}{}
b, err := json.Marshal(c)
b, err := json.Marshal(c.params)
if err != nil {
return m, err
}
err = json.Unmarshal(b, &m)
return m, err
}

func init() {
ingredients.RegisterAllMethods(Cmd{})
}
120 changes: 120 additions & 0 deletions ingredients/cmd/cmdRun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package cmd

import (
"context"
"errors"
"fmt"
"math"
"os/exec"
"os/user"
"runtime"
"strconv"
"strings"
"syscall"
"time"

"github.com/gogrlx/grlx/types"
)

func (c Cmd) run(ctx context.Context, test bool) (types.Result, error) {
var result types.Result
var err error

cmd, ok := c.params["name"].(string)
if !ok {
return result, errors.New("invalid command; name must be a string")
}
splitCmd := strings.Split(cmd, " ")
if len(splitCmd) == 0 {
return result, errors.New("invalid command; name must not be empty")
}
args := splitCmd[1:]

runas := ""
path := ""
cwd := ""
env := []string{}
timeout := ""
if runasInter, ok := c.params["runas"]; ok {
runas, ok = runasInter.(string)
}
if pathInter, ok := c.params["path"]; ok {
path, ok = pathInter.(string)
}
if cwdInter, ok := c.params["cwd"]; ok {
cwd, ok = cwdInter.(string)
}
if envInter, ok := c.params["env"]; ok {
env, ok = envInter.([]string)
}
if timeoutInter, ok := c.params["timeout"]; ok {
timeout, ok = timeoutInter.(string)
}
// sanity check env vars
envVars := map[string]string{}
for _, envVar := range env {
sp := strings.Split(envVar, "=")
if len(sp) != 2 {
return result, fmt.Errorf("invalid env var %s; vars must be key=value pairs", envVar)
}
envVars[sp[0]] = sp[1]
}
ttimeout, err := time.ParseDuration(timeout)
if err != nil {
return result, errors.Join(err, fmt.Errorf("invalid timeout %s; must be a valid duration", timeout))
}
timeoutCTX, cancel := context.WithTimeout(ctx, ttimeout)
defer cancel()
command := exec.CommandContext(timeoutCTX, splitCmd[0], args...)
if runas != "" && runtime.GOOS != "windows" {
u, err := user.Lookup(runas)
if err != nil {
return result, errors.Join(err, fmt.Errorf("invalid user %s; user must exist", runas))
}
uid64, err := strconv.Atoi(u.Uid)
if err != nil {
return result, errors.Join(err, fmt.Errorf("invalid user %s; user must exist", runas))
}
if uid64 > math.MaxInt32 {
return result, fmt.Errorf("UID %d is invalid", uid64)
}
uid := uint32(uid64)
command.SysProcAttr = &syscall.SysProcAttr{}
command.SysProcAttr.Credential = &syscall.Credential{Uid: uid}
}
if path != "" {
command.Path = path
}
if cwd != "" {
command.Dir = cwd
}
if len(envVars) > 0 {
command.Env = []string{}
for _, v := range env {
command.Env = append(command.Env, v)
}
}
if test {
result.Notes = append(result.Notes,
types.SimpleNote("Command would have been run"))
return result, nil
}

out, err := command.CombinedOutput()
result.Notes = append(result.Notes,
types.SimpleNote(fmt.Sprintf("Command output: %s", string(out))),
)

if err != nil {
result.Notes = append(result.Notes,
types.SimpleNote(fmt.Sprintf("Command failed: %s", err.Error())))
}
if command.ProcessState.ExitCode() != 0 {
result.Succeeded = false
result.Failed = true
} else {
result.Succeeded = true
result.Failed = false
}
return result, nil
}
11 changes: 8 additions & 3 deletions ingredients/cmd/run.go → ingredients/cmd/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ import (
"syscall"
"time"

"github.com/taigrr/log-socket/log"

"github.com/gogrlx/grlx/types"
nats "github.com/nats-io/nats.go"
"github.com/taigrr/log-socket/log"
)

var ec *nats.EncodedConn

func RegisterEC(encodedConn *nats.EncodedConn) {
ec = encodedConn
}

var envMutex sync.Mutex

// TODO allow selector to be more than an ID
func FRun(target types.KeyManager, cmdRun types.CmdRun) (types.CmdRun, error) {
topic := "grlx.sprouts." + target.SproutID + ".cmd.run"
var results types.CmdRun
Expand Down
7 changes: 5 additions & 2 deletions ingredients/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/gogrlx/grlx/types"
)

var ErrFileMethodUndefined = errors.New("file method undefined")

type File struct {
id string
method string
Expand Down Expand Up @@ -68,7 +70,7 @@ func (f File) undef() (types.Result, error) {
return types.Result{
Succeeded: false, Failed: true,
Changed: false, Notes: nil,
}, fmt.Errorf("method %s undefined", f.method)
}, errors.Join(ErrFileMethodUndefined, fmt.Errorf("method %s undefined", f.method))
}

func (f File) Test(ctx context.Context) (types.Result, error) {
Expand Down Expand Up @@ -419,6 +421,7 @@ func (f File) Apply(ctx context.Context) (types.Result, error) {

func (f File) PropertiesForMethod(method string) (map[string]string, error) {
switch f.method {
// TODO use ingredients.MethodPropsSet for remaining methods
case "absent":
return ingredients.MethodPropsSet{
ingredients.MethodProps{Key: "name", Type: "string", IsReq: true},
Expand Down Expand Up @@ -490,7 +493,7 @@ func (f File) PropertiesForMethod(method string) (map[string]string, error) {
}, nil
default:
// TODO define error type
return nil, fmt.Errorf("method %s undefined", f.method)
return nil, errors.Join(ErrDuplicateProtocol, fmt.Errorf("method %s undefined", f.method))

}
}
Expand Down
3 changes: 0 additions & 3 deletions ingredients/file/fileCached_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,6 @@ func TestCachedSkipVerify(t *testing.T) {
"skip_verify": true,
},
}
if err != nil {
t.Fatalf("failed to register local file provider: %v", err)
}
_, err = f.cached(context.Background(), false)
if err != nil {
t.Errorf("expected no error, got %v", err)
Expand Down

0 comments on commit f2b2b3a

Please sign in to comment.