Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions internal/pkg/runbook/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,36 @@ 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 {
cfg execConfig
}

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)
Expand All @@ -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
}
3 changes: 3 additions & 0 deletions internal/pkg/runbook/runbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}'
Expand Down