Skip to content

Commit 866b853

Browse files
feat: support query and path params passthrough
1 parent 13426b3 commit 866b853

2 files changed

Lines changed: 79 additions & 74 deletions

File tree

endpoint.go

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,33 @@ import (
99
"net/http"
1010
"os"
1111
"os/exec"
12+
"regexp"
1213
"slices"
1314
"strings"
1415
"syscall"
1516
"time"
1617
)
1718

1819
type resolvedEndpoint struct {
19-
method string
20-
command string
21-
args []string
22-
env []string
23-
timeout time.Duration
20+
method string
21+
command string
22+
args []string
23+
env []string
24+
timeout time.Duration
25+
pathParams []string
2426
}
2527

2628
type Endpoint struct {
27-
Path string `json:"path,omitempty" toml:"path,commented"`
28-
Token string `json:"token,omitempty" toml:"token,commented"`
29-
Method string `json:"method,omitempty" toml:"method,commented"`
30-
Cmd []string `json:"cmd,omitempty" toml:"cmd,commented"`
31-
Env []string `json:"env,omitempty" toml:"env,commented"`
32-
Detached bool `json:"detached,omitempty" toml:"detached,commented"`
33-
UID uint32 `json:"uid,omitempty" toml:"uid,commented"`
34-
GID uint32 `json:"gid,omitempty" toml:"gid,commented"`
35-
Timeout string `json:"timeout,omitempty" toml:"timeout,commented"`
36-
NoAuth bool `json:"no_auth,omitempty" toml:"no_auth,commented"`
29+
Path string `json:"path,omitempty" toml:"path,commented"`
30+
Token string `json:"token,omitempty" toml:"token,commented"`
31+
Method string `json:"method,omitempty" toml:"method,commented"`
32+
Cmd []string `json:"cmd,omitempty" toml:"cmd,commented"`
33+
EnvAllowlist []string `json:"env_allowlist,omitempty" toml:"env_allowlist,commented"`
34+
Detached bool `json:"detached,omitempty" toml:"detached,commented"`
35+
UID uint32 `json:"uid,omitempty" toml:"uid,commented"`
36+
GID uint32 `json:"gid,omitempty" toml:"gid,commented"`
37+
Timeout string `json:"timeout,omitempty" toml:"timeout,commented"`
38+
NoAuth bool `json:"no_auth,omitempty" toml:"no_auth,commented"`
3739

3840
resolvedEndpoint
3941
}
@@ -72,19 +74,28 @@ func (e *Endpoint) validate() error {
7274
return nil
7375
}
7476

77+
var re = regexp.MustCompile(`{([^{}]*)}`)
78+
7579
func (e *Endpoint) resolve() {
7680
e.method = cmp.Or(e.Method, http.MethodPost)
7781
e.command, e.args = e.Cmd[0], e.Cmd[1:]
7882

79-
e.env = make([]string, 0, len(e.Env))
80-
for _, key := range e.Env {
81-
e.env = append(e.env, key, os.Getenv(key))
83+
e.env = make([]string, 0, len(e.EnvAllowlist))
84+
for _, key := range e.EnvAllowlist {
85+
e.env = append(e.env, key+"="+os.Getenv(key))
8286
}
8387

8488
if e.Timeout != "" {
8589
t, _ := time.ParseDuration(e.Timeout) // validated at [Endpoint.validate]
8690
e.timeout = t
8791
}
92+
93+
e.pathParams = make([]string, 0, 4)
94+
95+
matches := re.FindAllStringSubmatch(e.Path, -1)
96+
for _, v := range matches {
97+
e.pathParams = append(e.pathParams, v[1])
98+
}
8899
}
89100

90101
func (e *Endpoint) redact() Endpoint {
@@ -107,15 +118,15 @@ type ExecResult struct {
107118
Error string `json:"error,omitempty"`
108119
}
109120

110-
func (e *Endpoint) run(ctx context.Context) *ExecResult {
121+
func (e *Endpoint) run(ctx context.Context, env []string) *ExecResult {
111122
if e.Detached {
112-
return e.runDetached()
123+
return e.runDetached(env)
113124
}
114125

115-
return e.runWait(ctx)
126+
return e.runWait(ctx, env)
116127
}
117128

118-
func (e *Endpoint) runWait(ctx context.Context) *ExecResult {
129+
func (e *Endpoint) runWait(ctx context.Context, env []string) *ExecResult {
119130
if e.timeout != 0 {
120131
c, cancel := context.WithTimeout(ctx, e.timeout)
121132
ctx = c
@@ -131,7 +142,7 @@ func (e *Endpoint) runWait(ctx context.Context) *ExecResult {
131142
cmd.Stdout = &stdout
132143
cmd.Stderr = &stderr
133144

134-
cmd.Env = e.env
145+
cmd.Env = slices.Concat(e.env, env)
135146

136147
if e.UID != 0 || e.GID != 0 {
137148
cmd.SysProcAttr = &syscall.SysProcAttr{}
@@ -162,9 +173,9 @@ func (e *Endpoint) runWait(ctx context.Context) *ExecResult {
162173
return &execResult
163174
}
164175

165-
func (e *Endpoint) runDetached() *ExecResult {
176+
func (e *Endpoint) runDetached(env []string) *ExecResult {
166177
cmd := exec.Command(e.command, e.args...) //nolint:gosec,noctx // command and args come from trusted config // noctx is intentional
167-
cmd.Env = e.env
178+
cmd.Env = slices.Concat(e.env, env)
168179

169180
if f, err := os.OpenFile("/dev/null", os.O_WRONLY, 0); err == nil {
170181
cmd.Stdout, cmd.Stderr, cmd.Stdin = f, f, nil

main.go

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"bytes"
45
"cmp"
56
"context"
67
"crypto/subtle"
@@ -16,45 +17,23 @@ import (
1617
"strings"
1718
"syscall"
1819
"time"
20+
"unicode"
1921

2022
"github.com/google/uuid"
2123
)
2224

2325
var Version = "v0.0.0"
2426

25-
type execState int
27+
type execState string
2628

2729
const (
28-
_ execState = iota
29-
execStateRunning
30-
execStateQueued
31-
execStateCompleted
32-
execStateFailed
33-
execStateCanceled
30+
execStateRunning execState = "running"
31+
execStateQueued execState = "queued"
32+
execStateCompleted execState = "completed"
33+
execStateFailed execState = "failed"
34+
execStateCanceled execState = "canceled"
3435
)
3536

36-
func (s execState) String() string {
37-
switch s {
38-
case execStateQueued:
39-
return "queued"
40-
case execStateRunning:
41-
return "running"
42-
case execStateCompleted:
43-
return "completed"
44-
case execStateFailed:
45-
return "failed"
46-
case execStateCanceled:
47-
return "canceled"
48-
default:
49-
return "unknown"
50-
}
51-
}
52-
53-
//nolint:unparam
54-
func (s execState) MarshalText() ([]byte, error) {
55-
return []byte(s.String()), nil
56-
}
57-
5837
const (
5938
defaultConfigName = ".execd.toml"
6039
defaultListenAddr = ":8081"
@@ -131,22 +110,6 @@ func newJobsHandler(jobs *safeMap[string, RequestState]) http.Handler {
131110
CompletedAt time.Time `json:"completed_at,omitzero"`
132111
}
133112

134-
compare := func(a, b JobsSummary) int {
135-
if a.State != b.State {
136-
return cmp.Compare(int(a.State), int(b.State))
137-
}
138-
139-
// descending order
140-
switch {
141-
case a.StartedAt.After(b.StartedAt):
142-
return -1
143-
case a.StartedAt.Before(b.StartedAt):
144-
return 1
145-
default:
146-
return 0
147-
}
148-
}
149-
150113
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151114
if r.Method != http.MethodGet {
152115
w.Header().Set("Allow", http.MethodGet)
@@ -171,7 +134,9 @@ func newJobsHandler(jobs *safeMap[string, RequestState]) http.Handler {
171134
})
172135
})
173136

174-
slices.SortFunc(summary, compare)
137+
slices.SortFunc(summary, func(a, b JobsSummary) int {
138+
return a.StartedAt.Compare(b.StartedAt)
139+
})
175140

176141
writeJSON(w, http.StatusOK, summary)
177142
})
@@ -212,6 +177,35 @@ func newJobHandler(jobs *safeMap[string, RequestState]) http.Handler {
212177
})
213178
}
214179

180+
func toEnvKey(s string) (key string) {
181+
buf := &bytes.Buffer{}
182+
183+
for i, r := range s {
184+
if unicode.IsUpper(r) && i > 0 {
185+
buf.WriteRune('_')
186+
}
187+
188+
buf.WriteRune(unicode.ToUpper(r))
189+
}
190+
191+
return buf.String()
192+
}
193+
194+
func paramsToEnv(r *http.Request, pathParams []string) []string {
195+
params := r.URL.Query()
196+
env := make([]string, 0, len(params)+len(pathParams))
197+
198+
for k, v := range params {
199+
env = append(env, toEnvKey(k)+"="+strings.Join(v, " "))
200+
}
201+
202+
for _, k := range pathParams {
203+
env = append(env, toEnvKey(k)+"="+r.PathValue(k))
204+
}
205+
206+
return env
207+
}
208+
215209
func newExecHandler(appCtx context.Context, e Endpoint, jobs *safeMap[string, RequestState]) http.Handler {
216210
method := strings.ToUpper(e.method)
217211

@@ -241,11 +235,11 @@ func newExecHandler(appCtx context.Context, e Endpoint, jobs *safeMap[string, Re
241235
ID string `json:"id,omitempty"`
242236
}{ID: id})
243237

244-
go runRequest(appCtx, e, jobs, id)
238+
go runRequest(appCtx, e, jobs, id, paramsToEnv(r, e.pathParams))
245239
})
246240
}
247241

248-
func runRequest(ctx context.Context, e Endpoint, jobs *safeMap[string, RequestState], id string) {
242+
func runRequest(ctx context.Context, e Endpoint, jobs *safeMap[string, RequestState], id string, env []string) {
249243
ctx, cancel := context.WithCancel(ctx)
250244
defer cancel()
251245

@@ -254,7 +248,7 @@ func runRequest(ctx context.Context, e Endpoint, jobs *safeMap[string, RequestSt
254248
return old
255249
})
256250

257-
execResult := e.run(ctx)
251+
execResult := e.run(ctx, env)
258252

259253
completed := RequestState{
260254
State: execStateCompleted,

0 commit comments

Comments
 (0)