Skip to content

Commit 5d5f9e5

Browse files
authored
feat: Embedded runbook expressions (#54)
Expands existing behaviour by requiring either `path` or `expr` in `exec block with optional `runtime` argument, passing the expression via stdin. If `runtime` isn't present the default to `sh`. ``` - type: exec exec: expr: [...] runtime: [...] ```
1 parent 2333504 commit 5d5f9e5

2 files changed

Lines changed: 53 additions & 10 deletions

File tree

internal/pkg/runbook/exec.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,36 @@ package runbook
33
import (
44
"bytes"
55
"context"
6-
"encoding/json"
76
"errors"
87
"os"
98
"os/exec"
9+
"strings"
1010
"text/template"
1111
)
1212

1313
type execConfig struct {
14-
Path string `yaml:"path"`
15-
Args []string `yaml:"args"`
14+
Path string `yaml:"path"`
15+
Expr string `yaml:"expr"`
16+
Runtime string `yaml:"runtime"` // optional, default to "sh -s"
17+
Args []string `yaml:"args"`
1618
}
1719

1820
type execAction struct {
1921
cfg execConfig
2022
}
2123

2224
func newExecAction(cfg execConfig) (Action, error) {
23-
if cfg.Path == "" {
24-
return nil, errors.New("exec.path is required")
25+
if cfg.Path == "" && cfg.Expr == "" {
26+
return nil, errors.New("either exec.path or exec.expr is required")
27+
}
28+
if cfg.Path != "" && cfg.Expr != "" {
29+
return nil, errors.New("exec.path and exec.expr are mutually exclusive")
2530
}
2631
return &execAction{cfg: cfg}, nil
2732
}
2833

2934
func (e *execAction) Execute(ctx context.Context, cre map[string]any) error {
35+
// Template substitution for args
3036
args := make([]string, len(e.cfg.Args))
3137
for i, a := range e.cfg.Args {
3238
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 {
3844
}
3945
}
4046

41-
raw, err := json.Marshal(cre)
42-
if err != nil {
43-
return err
47+
var cmd *exec.Cmd
48+
49+
switch {
50+
// External command with args
51+
case e.cfg.Path != "":
52+
cmd = exec.CommandContext(ctx, e.cfg.Path, args...)
53+
54+
// expr + runtime piped via stdin
55+
case e.cfg.Expr != "":
56+
// Expand template variables
57+
expr, err := renderTemplate(e.cfg.Expr, cre)
58+
if err != nil {
59+
return err
60+
}
61+
62+
runtime := e.cfg.Runtime
63+
if runtime == "" {
64+
runtime = "sh -s"
65+
}
66+
67+
parts := splitRuntime(runtime)
68+
cmd = exec.CommandContext(ctx, parts[0], append(parts[1:], args...)...)
69+
cmd.Stdin = strings.NewReader(expr)
4470
}
4571

46-
cmd := exec.CommandContext(ctx, e.cfg.Path, args...)
47-
cmd.Stdin = bytes.NewReader(raw)
72+
// Common output wiring
4873
cmd.Stdout = os.Stdout
4974
cmd.Stderr = os.Stderr
75+
5076
return cmd.Run()
5177
}
78+
79+
func renderTemplate(input string, data map[string]any) (string, error) {
80+
tmpl, err := template.New("inline").Funcs(funcMap()).Parse(input)
81+
if err != nil {
82+
return "", err
83+
}
84+
var buf bytes.Buffer
85+
err = tmpl.Execute(&buf, data)
86+
return buf.String(), err
87+
}
88+
89+
func splitRuntime(runtime string) []string {
90+
return strings.Fields(runtime) // basic split
91+
}

internal/pkg/runbook/runbook.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ actions:
2929
regex: "CRE-2025-0025"
3030
exec:
3131
path: ./action.sh
32+
expr: |
33+
echo "Critical incident: {{ field .cre "Id" }}"
34+
runtime: bash -
3235
args:
3336
- '{{ field .cre "Id" }}'
3437
- '{{ len .hits }}'

0 commit comments

Comments
 (0)