From bbaa71ea6b66093e4e1b526ced30081322e5e128 Mon Sep 17 00:00:00 2001 From: dkarczmarski Date: Fri, 6 Mar 2026 17:46:17 +0100 Subject: [PATCH] refactor(config): rename outputType to executionMode (buffered | stream | async) --- README.md | 14 ++--- config.sample-ssl.yaml | 6 +- config.sample.yaml | 6 +- pkg/config/config.go | 2 +- pkg/config/config_test.go | 60 ++++++++++++++++++- pkg/config/test-data/test-config.yaml | 2 +- pkg/router/handlers/execution_handler.go | 10 ++-- pkg/router/handlers/execution_handler_test.go | 42 ++++++------- test/integration/server_test.go | 6 +- test/integration/test-data/test-config.yaml | 6 +- 10 files changed, 105 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 93e65d6..00639d1 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ urlCommands: logs -f my-container - outputType: stream + executionMode: stream ``` Call the endpoint: @@ -200,7 +200,7 @@ urlCommands: - url: POST /maintenance/backup commandTemplate: | /usr/local/bin/backup.sh - outputType: none + executionMode: async timeout: 1h ``` @@ -332,11 +332,11 @@ Each entry contains: * `graceTerminationTimeout` *(optional)* The time to wait for the process to exit gracefully after sending `SIGTERM` when the context is cancelled (e.g., client disconnects or timeout occurs) before sending `SIGKILL`. Format: [Go Duration](https://pkg.go.dev/time#ParseDuration). Default: no grace period (sends `SIGKILL` immediately). -* `outputType` *(optional)* - Determines how the command output is returned: - - `text`: (default) returns the full output once the command finishes. - - `stream`: returns the output in real-time as it is produced by the command. - - `none`: executes the command asynchronously in the background. The HTTP response is sent immediately after the process starts, and any output is discarded. Note that the optional `timeout` is still respected for background processes. +* `executionMode` *(optional)* + Determines how the command is executed and how its output is returned: + - `buffered`: (default) run command synchronously and return the full output once it finishes. + - `stream`: run command synchronously and stream output in real-time as it is produced. + - `async`: start command and return immediately without waiting for it to finish. Any output is discarded. Note that the optional `timeout` is still respected for background processes. * `callGate` *(optional)* Controls the concurrency of command execution. It allows you to limit how many instances of a command (or a group of commands) can run simultaneously. diff --git a/config.sample-ssl.yaml b/config.sample-ssl.yaml index 9784f6b..eadbaa3 100644 --- a/config.sample-ssl.yaml +++ b/config.sample-ssl.yaml @@ -40,14 +40,14 @@ urlCommands: bash -c for i in {1..10}; do echo -n "$i "; date '+%H:%M:%S'; sleep 1; done - outputType: stream + executionMode: stream timeout: 30s - url: GET /stream/time-with-timeout-5 commandTemplate: | bash -c for i in {1..10}; do echo -n "$i "; date '+%H:%M:%S'; sleep 1; done - outputType: stream + executionMode: stream timeout: 5s - url: GET /cmd/sleep callGate: @@ -55,7 +55,7 @@ urlCommands: commandTemplate: | /bin/sleep 5 - outputType: none + executionMode: async timeout: 10s - url: POST /maintenance/cleanup callGate: diff --git a/config.sample.yaml b/config.sample.yaml index 3ce1be4..6801572 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -36,14 +36,14 @@ urlCommands: bash -c for i in {1..10}; do echo -n "$i "; date '+%H:%M:%S'; sleep 1; done - outputType: stream + executionMode: stream timeout: 30s - url: GET /stream/time-with-timeout-5 commandTemplate: | bash -c for i in {1..10}; do echo -n "$i "; date '+%H:%M:%S'; sleep 1; done - outputType: stream + executionMode: stream timeout: 5s - url: GET /cmd/sleep callGate: @@ -51,7 +51,7 @@ urlCommands: commandTemplate: | /bin/sleep 5 - outputType: none + executionMode: async timeout: 10s - url: POST /maintenance/cleanup callGate: diff --git a/pkg/config/config.go b/pkg/config/config.go index 3d27e9a..4c755b3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -63,7 +63,7 @@ type CommandConfig struct { Params ParamsConfig `yaml:"params"` Timeout *time.Duration `yaml:"timeout"` GraceTerminationTimeout *time.Duration `yaml:"graceTerminationTimeout"` - OutputType string `yaml:"outputType"` + ExecutionMode string `yaml:"executionMode"` CallGate *CallGateConfig `yaml:"callGate"` } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 12bfb87..1fc489e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -54,8 +54,8 @@ func TestLoadConfig(t *testing.T) { Params: config.ParamsConfig{ BodyAsJSON: ptrBool(true), }, - Timeout: ptrDuration(5 * time.Second), - OutputType: "text", + Timeout: ptrDuration(5 * time.Second), + ExecutionMode: "buffered", }, }, { @@ -361,6 +361,62 @@ urlCommands: } } +func TestExecutionMode(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + yaml string + expectedMode string + }{ + { + name: "explicit executionMode buffered", + yaml: ` +urlCommands: + - url: POST /test + commandTemplate: /bin/echo + executionMode: buffered +`, + expectedMode: "buffered", + }, + { + name: "explicit executionMode stream", + yaml: ` +urlCommands: + - url: POST /test + commandTemplate: /bin/echo + executionMode: stream +`, + expectedMode: "stream", + }, + { + name: "explicit executionMode async", + yaml: ` +urlCommands: + - url: POST /test + commandTemplate: /bin/echo + executionMode: async +`, + expectedMode: "async", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cfg, err := config.LoadConfigFromString(tc.yaml) + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + if cfg.URLCommands[0].ExecutionMode != tc.expectedMode { + t.Errorf("expected ExecutionMode %q, got %q", tc.expectedMode, cfg.URLCommands[0].ExecutionMode) + } + }) + } +} + func ptrBool(b bool) *bool { return &b } diff --git a/pkg/config/test-data/test-config.yaml b/pkg/config/test-data/test-config.yaml index bf6397b..6d44a05 100644 --- a/pkg/config/test-data/test-config.yaml +++ b/pkg/config/test-data/test-config.yaml @@ -19,7 +19,7 @@ urlCommands: params: bodyAsJson: true timeout: 5s - outputType: text + executionMode: buffered - url: POST /cmd/sleep commandTemplate: | /usr/bin/sleep diff --git a/pkg/router/handlers/execution_handler.go b/pkg/router/handlers/execution_handler.go index da17978..85f88c8 100644 --- a/pkg/router/handlers/execution_handler.go +++ b/pkg/router/handlers/execution_handler.go @@ -365,17 +365,17 @@ func prepareOutputAndRunCommand( cmdResult *cmdbuilder.Result, responseWriter http.ResponseWriter, ) error { - outputType := cmd.CommandConfig.OutputType + executionMode := cmd.CommandConfig.ExecutionMode - switch outputType { - case "none": + switch executionMode { + case "async": return prepareOutputAndRunAsyncCommand(ctx, starter, exec, cmd, cmdResult, responseWriter) case "stream": return prepareOutputAndRunStreamCommand(ctx, starter, exec, cmd, cmdResult, responseWriter) - case "", "text": + case "", "buffered": return prepareOutputAndRunSyncCommand(ctx, starter, exec, cmd, cmdResult, responseWriter) default: - return fmt.Errorf("%w: unknown output type %q", ErrBadConfiguration, outputType) + return fmt.Errorf("%w: unknown execution mode %q", ErrBadConfiguration, executionMode) } } diff --git a/pkg/router/handlers/execution_handler_test.go b/pkg/router/handlers/execution_handler_test.go index 7140bbe..d1b185f 100644 --- a/pkg/router/handlers/execution_handler_test.go +++ b/pkg/router/handlers/execution_handler_test.go @@ -40,7 +40,7 @@ func TestExecutionHandler_HappyPath_Stream(t *testing.T) { URL: "POST /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo\n{{.url.name}}\n{{.headers.X_Test}}\n{{.body.text}}", - OutputType: "stream", + ExecutionMode: "stream", }, } @@ -101,7 +101,7 @@ func TestExecutionHandler_Text_EmptyBody(t *testing.T) { URL: "POST /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo '{{.body.text}}'", - OutputType: "text", + ExecutionMode: "buffered", }, } @@ -162,7 +162,7 @@ func TestExecutionHandler_ExtractParams_Query_FirstValueOnly(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo {{.url.a}}", - OutputType: "text", + ExecutionMode: "buffered", }, } @@ -196,7 +196,7 @@ func TestExecutionHandler_ExtractParams_Headers_NormalizeAndJoin(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo '{{.headers.X_Test_Header}}' '{{.headers.X_Test}}'", - OutputType: "text", + ExecutionMode: "buffered", }, } @@ -393,7 +393,7 @@ func TestExecutionHandler_Stream_RequiresFlusher_500(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "stream", + ExecutionMode: "stream", }, } @@ -416,7 +416,7 @@ func TestExecutionHandler_Stream_RequiresFlusher_500(t *testing.T) { } } -func TestExecutionHandler_UnknownOutputType_500(t *testing.T) { +func TestExecutionHandler_UnknownExecutionMode_500(t *testing.T) { t.Parallel() fr := &fakeRunner{} @@ -430,7 +430,7 @@ func TestExecutionHandler_UnknownOutputType_500(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "invalid", + ExecutionMode: "invalid", }, } @@ -445,12 +445,12 @@ func TestExecutionHandler_UnknownOutputType_500(t *testing.T) { } msg := rr.Header().Get("X-Error-Message") - if !strings.Contains(msg, "unknown output type") { - t.Fatalf("expected X-Error-Message to contain %q, got %q", "unknown output type", msg) + if !strings.Contains(msg, "unknown execution mode") { + t.Fatalf("expected X-Error-Message to contain %q, got %q", "unknown execution mode", msg) } } -func TestExecutionHandler_OutputNone_ReturnsBeforeWait(t *testing.T) { +func TestExecutionHandler_ExecutionModeAsync_ReturnsBeforeWait(t *testing.T) { t.Parallel() block := make(chan struct{}) @@ -467,7 +467,7 @@ func TestExecutionHandler_OutputNone_ReturnsBeforeWait(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "none", + ExecutionMode: "async", }, } @@ -486,7 +486,7 @@ func TestExecutionHandler_OutputNone_ReturnsBeforeWait(t *testing.T) { case <-done: case <-time.After(80 * time.Millisecond): close(block) - t.Fatalf("handler did not return quickly for outputType=none") + t.Fatalf("handler did not return quickly for executionMode=async") } if rr.Code != http.StatusOK { @@ -516,7 +516,7 @@ func TestExecutionHandler_CallGate_InvalidMode_500(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: ptrString("test-group"), Mode: "invalid-mode", @@ -554,7 +554,7 @@ func TestExecutionHandler_CallGate_Busy_429(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: ptrString("test-group"), Mode: "single", @@ -598,7 +598,7 @@ func TestExecutionHandler_CallGate_ImplicitGroupName_IsolatesDifferentURLs(t *te URL: "GET /exec1", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: nil, Mode: "single", @@ -609,7 +609,7 @@ func TestExecutionHandler_CallGate_ImplicitGroupName_IsolatesDifferentURLs(t *te URL: "GET /exec2", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: nil, Mode: "single", @@ -653,7 +653,7 @@ func TestExecutionHandler_CallGate_EmptyGroupName_SharedAcrossURLs(t *testing.T) URL: "GET /exec1", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: ptrString(""), Mode: "single", @@ -664,7 +664,7 @@ func TestExecutionHandler_CallGate_EmptyGroupName_SharedAcrossURLs(t *testing.T) URL: "GET /exec2", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: ptrString(""), Mode: "single", @@ -710,7 +710,7 @@ func TestExecutionHandler_CallGate_SharedGroupName_SharedAcrossURLs(t *testing.T URL: "GET /exec1", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: &shared, Mode: "single", @@ -721,7 +721,7 @@ func TestExecutionHandler_CallGate_SharedGroupName_SharedAcrossURLs(t *testing.T URL: "GET /exec2", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", CallGate: &config.CallGateConfig{ GroupName: &shared, Mode: "single", @@ -781,7 +781,7 @@ func TestExecutionHandler_NonZeroExit_SetsExitCodeHeader(t *testing.T) { URL: "GET /exec", CommandConfig: config.CommandConfig{ CommandTemplate: "echo hello", - OutputType: "text", + ExecutionMode: "buffered", }, } diff --git a/test/integration/server_test.go b/test/integration/server_test.go index 156cf3d..2fb4053 100644 --- a/test/integration/server_test.go +++ b/test/integration/server_test.go @@ -424,7 +424,7 @@ func TestServer_OutputTypes(t *testing.T) { } }) - t.Run("GET /cmd/bad-output - unknown output type returns 500", func(t *testing.T) { + t.Run("GET /cmd/bad-output - unknown execution mode returns 500", func(t *testing.T) { t.Parallel() srv := setupServer(t) @@ -438,8 +438,8 @@ func TestServer_OutputTypes(t *testing.T) { } errMsg := rec.Header().Get("X-Error-Message") - if errMsg == "" || !strings.Contains(errMsg, "unknown output type") { - t.Fatalf("Expected X-Error-Message to contain %q, got %q", "unknown output type", errMsg) + if errMsg == "" || !strings.Contains(errMsg, "unknown execution mode") { + t.Fatalf("Expected X-Error-Message to contain %q, got %q", "unknown execution mode", errMsg) } }) } diff --git a/test/integration/test-data/test-config.yaml b/test/integration/test-data/test-config.yaml index e198b91..98f9d19 100644 --- a/test/integration/test-data/test-config.yaml +++ b/test/integration/test-data/test-config.yaml @@ -48,12 +48,12 @@ urlCommands: commandTemplate: | /bin/echo stream-output - outputType: stream + executionMode: stream - url: GET /cmd/none commandTemplate: | /bin/echo no-output - outputType: none + executionMode: async - url: GET /cmd/query-first @@ -83,7 +83,7 @@ urlCommands: commandTemplate: | /bin/echo hello - outputType: invalid + executionMode: invalid timeout: 5s - url: GET /cmd/bad-template