Skip to content

Commit 09642fb

Browse files
committed
Add ability to stream command output as a response
1 parent 345bf3d commit 09642fb

File tree

3 files changed

+45
-8
lines changed

3 files changed

+45
-8
lines changed

docs/Hook-Definition.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Hooks are defined as JSON objects. Please note that in order to be considered va
1313
* `http-methods` - a list of allowed HTTP methods, such as `POST` and `GET`
1414
* `include-command-output-in-response` - boolean whether webhook should wait for the command to finish and return the raw output as a response to the hook initiator. If the command fails to execute or encounters any errors while executing the response will result in 500 Internal Server Error HTTP status code, otherwise the 200 OK status code will be returned.
1515
* `include-command-output-in-response-on-error` - boolean whether webhook should include command stdout & stderror as a response in failed executions. It only works if `include-command-output-in-response` is set to `true`.
16+
* `stream-command-output` - boolean whether webhook should stream command stdout & stderror as a response. If true `include-command-output-in-response` is ignored.
1617
* `parse-parameters-as-json` - specifies the list of arguments that contain JSON strings. These parameters will be decoded by webhook and you can access them like regular objects in rules and `pass-arguments-to-command`.
1718
* `pass-arguments-to-command` - specifies the list of arguments that will be passed to the command. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
1819
`{ "source": "string", "name": "argumentvalue" }`

internal/hook/hook.go

+1
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ type Hook struct {
508508
ResponseMessage string `json:"response-message,omitempty"`
509509
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
510510
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
511+
StreamCommandOutput bool `json:"stream-command-output,omitempty"`
511512
CaptureCommandOutputOnError bool `json:"include-command-output-in-response-on-error,omitempty"`
512513
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
513514
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`

webhook.go

+43-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"flag"
88
"fmt"
9+
"io"
910
"io/ioutil"
1011
"log"
1112
"net"
@@ -66,6 +67,19 @@ var (
6667
pidFile *pidfile.PIDFile
6768
)
6869

70+
type flushWriter struct {
71+
f http.Flusher
72+
w io.Writer
73+
}
74+
75+
func (fw *flushWriter) Write(p []byte) (n int, err error) {
76+
n, err = fw.w.Write(p)
77+
if fw.f != nil {
78+
fw.f.Flush()
79+
}
80+
return
81+
}
82+
6983
func matchLoadedHook(id string) *hook.Hook {
7084
for _, hooks := range loadedHooksFromFiles {
7185
if hook := hooks.Match(id); hook != nil {
@@ -514,8 +528,10 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
514528
w.Header().Set(responseHeader.Name, responseHeader.Value)
515529
}
516530

517-
if matchedHook.CaptureCommandOutput {
518-
response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body)
531+
if matchedHook.StreamCommandOutput {
532+
handleHook(matchedHook, rid, &headers, &query, &payload, &body, w)
533+
} else if matchedHook.CaptureCommandOutput {
534+
response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body, nil)
519535

520536
if err != nil {
521537
w.WriteHeader(http.StatusInternalServerError)
@@ -533,7 +549,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
533549
fmt.Fprint(w, response)
534550
}
535551
} else {
536-
go handleHook(matchedHook, rid, &headers, &query, &payload, &body)
552+
go handleHook(matchedHook, rid, &headers, &query, &payload, &body, nil)
537553

538554
// Check if a success return code is configured for the hook
539555
if matchedHook.SuccessHttpResponseCode != 0 {
@@ -556,7 +572,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
556572
fmt.Fprint(w, "Hook rules were not satisfied.")
557573
}
558574

559-
func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]interface{}, body *[]byte) (string, error) {
575+
func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]interface{}, body *[]byte, w http.ResponseWriter) (string, error) {
560576
var errors []error
561577

562578
// check the command exists
@@ -625,12 +641,31 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
625641

626642
log.Printf("[%s] executing %s (%s) with arguments %q and environment %s using %s as cwd\n", rid, h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
627643

628-
out, err := cmd.CombinedOutput()
644+
var out []byte
629645

630-
log.Printf("[%s] command output: %s\n", rid, out)
646+
if w != nil {
647+
log.Printf("[%s] command output will be streamed to response", rid)
631648

632-
if err != nil {
633-
log.Printf("[%s] error occurred: %+v\n", rid, err)
649+
// Implementation from https://play.golang.org/p/PpbPyXbtEs
650+
// as described in https://stackoverflow.com/questions/19292113/not-buffered-http-responsewritter-in-golang
651+
fw := flushWriter{w: w}
652+
if f, ok := w.(http.Flusher); ok {
653+
fw.f = f
654+
}
655+
cmd.Stderr = &fw
656+
cmd.Stdout = &fw
657+
658+
if err := cmd.Run(); err != nil {
659+
log.Printf("[%s] error occurred: %+v\n", rid, err)
660+
}
661+
} else {
662+
out, err = cmd.CombinedOutput()
663+
664+
log.Printf("[%s] command output: %s\n", rid, out)
665+
666+
if err != nil {
667+
log.Printf("[%s] error occurred: %+v\n", rid, err)
668+
}
634669
}
635670

636671
for i := range files {

0 commit comments

Comments
 (0)