From e801b013f41706c682b718b230af556adfd3b1e7 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Tue, 28 Jul 2026 11:45:29 -0400 Subject: [PATCH] Disable global timeout for --server mode The default 5-minute global timeout was killing the persistent HTTP server started by `ec validate input --server`. The timeout is now automatically disabled when --server is used. Also add missing unit build tag to root_cmd_test.go Ref: https://redhat.atlassian.net/browse/EC-2029 Co-Authored-By: Claude Opus 4.6 --- cmd/root/root_cmd.go | 26 ++++++++++++++++++++++---- cmd/root/root_cmd_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/cmd/root/root_cmd.go b/cmd/root/root_cmd.go index 2c4246c8e..960a62a1d 100644 --- a/cmd/root/root_cmd.go +++ b/cmd/root/root_cmd.go @@ -63,6 +63,23 @@ func (customDeadlineExceededError) Error() string { func (customDeadlineExceededError) Timeout() bool { return true } func (customDeadlineExceededError) Temporary() bool { return true } +// effectiveTimeout returns the timeout to use for the given command. It +// disables the timeout for "validate input --server" since that starts a +// persistent HTTP server that should run indefinitely. +// +// Note: this couples the root command to a leaf subcommand's flag. Moving +// the logic to the subcommand's PreRunE is not practical because +// PersistentPreRun has already applied the timeout to the context by then. +func effectiveTimeout(cmd *cobra.Command, timeout time.Duration) time.Duration { + if cmd.Name() == "input" && cmd.Parent() != nil && cmd.Parent().Name() == "validate" { + if serverFlag := cmd.Flags().Lookup("server"); serverFlag != nil && serverFlag.Value.String() == "true" { + log.Debug("timeout disabled because --server flag is set") + return 0 + } + } + return timeout +} + func NewRootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "ec", @@ -116,11 +133,12 @@ func NewRootCmd() *cobra.Command { // custom timeout can be used and traces can be added ctx := cmd.Context() var cancel context.CancelFunc - if globalTimeout > 0 { - ctx, cancel = context.WithTimeout(ctx, globalTimeout) - log.Debugf("globalTimeout is %s", time.Duration(globalTimeout)) + timeout := effectiveTimeout(cmd, globalTimeout) + if timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, timeout) + log.Debugf("globalTimeout is %s", time.Duration(timeout)) } else { - log.Debugf("globalTimeout is %d, no timeout used", globalTimeout) + log.Debugf("globalTimeout is %d, no timeout used", timeout) } ctx = tracing.WithTrace(ctx, enabledTraces) cmd.SetContext(ctx) diff --git a/cmd/root/root_cmd_test.go b/cmd/root/root_cmd_test.go index 654c7947f..3398915b6 100644 --- a/cmd/root/root_cmd_test.go +++ b/cmd/root/root_cmd_test.go @@ -14,12 +14,16 @@ // // SPDX-License-Identifier: Apache-2.0 +//go:build unit + package root import ( + "context" "testing" "time" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/conforma/cli/internal/http" @@ -79,6 +83,38 @@ func TestGlobalTimeout(t *testing.T) { } } +func TestGlobalTimeoutDisabledInServerMode(t *testing.T) { + globalTimeout = 5 * time.Minute + + var capturedCtx context.Context + cmd := NewRootCmd() + + // Synthetic command tree to avoid pulling in the real validate input + // command and its dependencies. If the real flag or command name changes, + // the server-mode acceptance tests would catch it. + validate := &cobra.Command{Use: "validate"} + input := &cobra.Command{ + Use: "input", + Run: func(cmd *cobra.Command, _ []string) { + capturedCtx = cmd.Context() + }, + } + var server bool + input.Flags().BoolVar(&server, "server", false, "") + validate.AddCommand(input) + cmd.AddCommand(validate) + + cmd.SetArgs([]string{"validate", "input", "--server"}) + + err := cmd.Execute() + assert.NoError(t, err) + // The package-level variable must not be mutated. + assert.Equal(t, 5*time.Minute, globalTimeout) + // The execution context must not carry a deadline. + _, hasDeadline := capturedCtx.Deadline() + assert.False(t, hasDeadline) +} + func TestRetryConfigurationFlags(t *testing.T) { tests := []struct { name string