Skip to content

Commit

Permalink
Add ability to stream command output as a response
Browse files Browse the repository at this point in the history
  • Loading branch information
milolav committed Jul 14, 2020
1 parent 345bf3d commit 09642fb
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/Hook-Definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Hooks are defined as JSON objects. Please note that in order to be considered va
* `http-methods` - a list of allowed HTTP methods, such as `POST` and `GET`
* `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.
* `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`.
* `stream-command-output` - boolean whether webhook should stream command stdout & stderror as a response. If true `include-command-output-in-response` is ignored.
* `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`.
* `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
`{ "source": "string", "name": "argumentvalue" }`
Expand Down
1 change: 1 addition & 0 deletions internal/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ type Hook struct {
ResponseMessage string `json:"response-message,omitempty"`
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
StreamCommandOutput bool `json:"stream-command-output,omitempty"`
CaptureCommandOutputOnError bool `json:"include-command-output-in-response-on-error,omitempty"`
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
Expand Down
51 changes: 43 additions & 8 deletions webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
Expand Down Expand Up @@ -66,6 +67,19 @@ var (
pidFile *pidfile.PIDFile
)

type flushWriter struct {
f http.Flusher
w io.Writer
}

func (fw *flushWriter) Write(p []byte) (n int, err error) {
n, err = fw.w.Write(p)
if fw.f != nil {
fw.f.Flush()
}
return
}

func matchLoadedHook(id string) *hook.Hook {
for _, hooks := range loadedHooksFromFiles {
if hook := hooks.Match(id); hook != nil {
Expand Down Expand Up @@ -514,8 +528,10 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set(responseHeader.Name, responseHeader.Value)
}

if matchedHook.CaptureCommandOutput {
response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body)
if matchedHook.StreamCommandOutput {
handleHook(matchedHook, rid, &headers, &query, &payload, &body, w)
} else if matchedHook.CaptureCommandOutput {
response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body, nil)

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

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

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

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

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)

out, err := cmd.CombinedOutput()
var out []byte

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

if err != nil {
log.Printf("[%s] error occurred: %+v\n", rid, err)
// Implementation from https://play.golang.org/p/PpbPyXbtEs
// as described in https://stackoverflow.com/questions/19292113/not-buffered-http-responsewritter-in-golang
fw := flushWriter{w: w}
if f, ok := w.(http.Flusher); ok {
fw.f = f
}
cmd.Stderr = &fw
cmd.Stdout = &fw

if err := cmd.Run(); err != nil {
log.Printf("[%s] error occurred: %+v\n", rid, err)
}
} else {
out, err = cmd.CombinedOutput()

log.Printf("[%s] command output: %s\n", rid, out)

if err != nil {
log.Printf("[%s] error occurred: %+v\n", rid, err)
}
}

for i := range files {
Expand Down

0 comments on commit 09642fb

Please sign in to comment.