1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
package gomplate
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"text/template"
"time"
"github.com/hairyhenderson/gomplate/v3/conv"
"github.com/hairyhenderson/gomplate/v3/internal/config"
)
func bindPlugins(ctx context.Context, cfg *config.Config, funcMap template.FuncMap) error {
for k, v := range cfg.Plugins {
timeout := cfg.PluginTimeout
if v.Timeout != 0 {
timeout = v.Timeout
}
plugin := &plugin{
ctx: ctx,
name: k,
path: v.Cmd,
timeout: timeout,
pipe: v.Pipe,
stderr: cfg.Stderr,
}
if _, ok := funcMap[plugin.name]; ok {
return fmt.Errorf("function %q is already bound, and can not be overridden", plugin.name)
}
funcMap[plugin.name] = plugin.run
}
return nil
}
// plugin represents a custom function that binds to an external process to be executed
type plugin struct {
ctx context.Context
stderr io.Writer
name, path string
timeout time.Duration
pipe bool
}
// builds a command that's appropriate for running scripts
// nolint: gosec
func (p *plugin) buildCommand(a []string) (name string, args []string) {
switch filepath.Ext(p.path) {
case ".ps1":
a = append([]string{"-File", p.path}, a...)
return findPowershell(), a
case ".cmd", ".bat":
a = append([]string{"/c", p.path}, a...)
return "cmd.exe", a
default:
return p.path, a
}
}
// finds the appropriate powershell command for the platform - prefers
// PowerShell Core (`pwsh`), but on Windows if it's not found falls back to
// Windows PowerShell (`powershell`).
func findPowershell() string {
if runtime.GOOS != "windows" {
return "pwsh"
}
_, err := exec.LookPath("pwsh")
if err != nil {
return "powershell"
}
return "pwsh"
}
func (p *plugin) run(args ...interface{}) (interface{}, error) {
a := conv.ToStrings(args...)
name, a := p.buildCommand(a)
ctx, cancel := context.WithTimeout(p.ctx, p.timeout)
defer cancel()
var stdin *bytes.Buffer
if p.pipe && len(a) > 0 {
stdin = bytes.NewBufferString(a[len(a)-1])
a = a[:len(a)-1]
}
c := exec.CommandContext(ctx, name, a...)
if stdin != nil {
c.Stdin = stdin
}
c.Stderr = p.stderr
outBuf := &bytes.Buffer{}
c.Stdout = outBuf
start := time.Now()
err := c.Start()
if err != nil {
return nil, err
}
// make sure all signals are propagated
sigs := make(chan os.Signal, 1)
signal.Notify(sigs)
go func() {
select {
case sig := <-sigs:
// Pass signals to the sub-process
if c.Process != nil {
// nolint: gosec
_ = c.Process.Signal(sig)
}
case <-ctx.Done():
}
}()
err = c.Wait()
elapsed := time.Since(start)
if ctx.Err() != nil {
err = fmt.Errorf("plugin timed out after %v: %w", elapsed, ctx.Err())
}
return outBuf.String(), err
}
|