diff --git a/internal/pkg/runbook/exec.go b/internal/pkg/runbook/exec.go index 9673da4..7bea416 100644 --- a/internal/pkg/runbook/exec.go +++ b/internal/pkg/runbook/exec.go @@ -3,16 +3,18 @@ package runbook import ( "bytes" "context" - "encoding/json" "errors" "os" "os/exec" + "strings" "text/template" ) type execConfig struct { - Path string `yaml:"path"` - Args []string `yaml:"args"` + Path string `yaml:"path"` + Expr string `yaml:"expr"` + Runtime string `yaml:"runtime"` // optional, default to "sh -s" + Args []string `yaml:"args"` } type execAction struct { @@ -20,13 +22,17 @@ type execAction struct { } func newExecAction(cfg execConfig) (Action, error) { - if cfg.Path == "" { - return nil, errors.New("exec.path is required") + if cfg.Path == "" && cfg.Expr == "" { + return nil, errors.New("either exec.path or exec.expr is required") + } + if cfg.Path != "" && cfg.Expr != "" { + return nil, errors.New("exec.path and exec.expr are mutually exclusive") } return &execAction{cfg: cfg}, nil } func (e *execAction) Execute(ctx context.Context, cre map[string]any) error { + // Template substitution for args args := make([]string, len(e.cfg.Args)) for i, a := range e.cfg.Args { tmpl, err := template.New("arg").Funcs(funcMap()).Parse(a) @@ -38,14 +44,48 @@ func (e *execAction) Execute(ctx context.Context, cre map[string]any) error { } } - raw, err := json.Marshal(cre) - if err != nil { - return err + var cmd *exec.Cmd + + switch { + // External command with args + case e.cfg.Path != "": + cmd = exec.CommandContext(ctx, e.cfg.Path, args...) + + // expr + runtime piped via stdin + case e.cfg.Expr != "": + // Expand template variables + expr, err := renderTemplate(e.cfg.Expr, cre) + if err != nil { + return err + } + + runtime := e.cfg.Runtime + if runtime == "" { + runtime = "sh -s" + } + + parts := splitRuntime(runtime) + cmd = exec.CommandContext(ctx, parts[0], append(parts[1:], args...)...) + cmd.Stdin = strings.NewReader(expr) } - cmd := exec.CommandContext(ctx, e.cfg.Path, args...) - cmd.Stdin = bytes.NewReader(raw) + // Common output wiring cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + return cmd.Run() } + +func renderTemplate(input string, data map[string]any) (string, error) { + tmpl, err := template.New("inline").Funcs(funcMap()).Parse(input) + if err != nil { + return "", err + } + var buf bytes.Buffer + err = tmpl.Execute(&buf, data) + return buf.String(), err +} + +func splitRuntime(runtime string) []string { + return strings.Fields(runtime) // basic split +} diff --git a/internal/pkg/runbook/runbook.go b/internal/pkg/runbook/runbook.go index 557cf54..3719d67 100644 --- a/internal/pkg/runbook/runbook.go +++ b/internal/pkg/runbook/runbook.go @@ -29,6 +29,9 @@ actions: regex: "CRE-2025-0025" exec: path: ./action.sh + expr: | + echo "Critical incident: {{ field .cre "Id" }}" + runtime: bash - args: - '{{ field .cre "Id" }}' - '{{ len .hits }}'