diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b54774cad..d2f51d3b86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased +- Added ability to make the Resty HTTP Timeout configurable. ### Added - The `sensu.io/managed_by` label is now automatically applied to resources diff --git a/cli/client/client.go b/cli/client/client.go index 25e8fbaa43..138accb7f8 100644 --- a/cli/client/client.go +++ b/cli/client/client.go @@ -40,7 +40,7 @@ func New(config config.Config) *RestClient { client := &RestClient{resty: restyInst, config: config} // set http client timeout - restyInst.SetTimeout(15 * time.Second) + restyInst.SetTimeout(config.Timeout()) // Standardize redirect policy restyInst.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10)) diff --git a/cli/client/config/basic/basic.go b/cli/client/config/basic/basic.go index 268d29be59..c8b8574613 100644 --- a/cli/client/config/basic/basic.go +++ b/cli/client/config/basic/basic.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "time" "github.com/sensu/sensu-go/cli/commands/helpers" "github.com/sensu/sensu-go/types" @@ -34,6 +35,7 @@ type Cluster struct { TrustedCAFile string `json:"trusted-ca-file"` InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify"` *types.Tokens + Timeout time.Duration `json:"timeout"` } // Profile contains the active configuration @@ -110,6 +112,16 @@ func (c *Config) flags(flags *pflag.FlagSet) { if value, err := flags.GetString("trusted-ca-file"); err == nil && value != "" { c.Cluster.TrustedCAFile = value } + + if value, err := flags.GetString("timeout"); err == nil && value != "" { + duration, err := time.ParseDuration(value) + if err == nil { + c.Cluster.Timeout = duration + } else { + // Default to timeout of 15 seconds + c.Cluster.Timeout = 15 * time.Second + } + } } func (c *Config) open(path string) error { diff --git a/cli/client/config/basic/reader.go b/cli/client/config/basic/reader.go index a659335c47..030627d21d 100644 --- a/cli/client/config/basic/reader.go +++ b/cli/client/config/basic/reader.go @@ -1,6 +1,8 @@ package basic import ( + "time" + "github.com/sensu/sensu-go/cli/client/config" "github.com/sensu/sensu-go/types" ) @@ -31,6 +33,14 @@ func (c *Config) Namespace() string { return c.Profile.Namespace } +// Timeout returns the configured timeout +func (c *Config) Timeout() time.Duration { + if c.Cluster.Timeout == 0*time.Second { + return config.DefaultTimeout + } + return c.Cluster.Timeout +} + // Tokens returns the active cluster JWT func (c *Config) Tokens() *types.Tokens { return c.Cluster.Tokens diff --git a/cli/client/config/basic/reader_test.go b/cli/client/config/basic/reader_test.go index bd6594e46b..05ceab150e 100644 --- a/cli/client/config/basic/reader_test.go +++ b/cli/client/config/basic/reader_test.go @@ -2,6 +2,7 @@ package basic import ( "testing" + "time" "github.com/sensu/sensu-go/cli/client/config" "github.com/sensu/sensu-go/types" @@ -33,6 +34,16 @@ func TestNamespaceDefault(t *testing.T) { assert.Equal(t, config.DefaultNamespace, conf.Namespace()) } +func TestTimeout(t *testing.T) { + conf := &Config{Cluster: Cluster{Timeout: 30 * time.Second}} + assert.Equal(t, conf.Cluster.Timeout, conf.Timeout()) +} + +func TestTimeoutDefault(t *testing.T) { + conf := &Config{} + assert.Equal(t, config.DefaultTimeout, conf.Timeout()) +} + func TestTokens(t *testing.T) { tokens := &types.Tokens{Access: "foobar"} conf := &Config{Cluster: Cluster{Tokens: tokens}} diff --git a/cli/client/config/basic/writer.go b/cli/client/config/basic/writer.go index 859a7543fc..08ea6916e8 100644 --- a/cli/client/config/basic/writer.go +++ b/cli/client/config/basic/writer.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "time" "github.com/sensu/sensu-go/types" ) @@ -37,6 +38,13 @@ func (c *Config) SaveNamespace(namespace string) error { return write(c.Profile, filepath.Join(c.path, profileFilename)) } +// SaveTimeout saves the user's timeout to a configuration file +func (c *Config) SaveTimeout(timeout time.Duration) error { + c.Cluster.Timeout = timeout + + return write(c.Cluster, filepath.Join(c.path, clusterFilename)) +} + // SaveTokens saves the JWT into a configuration file func (c *Config) SaveTokens(tokens *types.Tokens) error { // Update the configuration loaded in memory diff --git a/cli/client/config/basic/writer_test.go b/cli/client/config/basic/writer_test.go index 392f9d1d05..2d444c2d85 100644 --- a/cli/client/config/basic/writer_test.go +++ b/cli/client/config/basic/writer_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/sensu/sensu-go/types" "github.com/spf13/pflag" @@ -75,6 +76,21 @@ func TestSaveNamespace(t *testing.T) { assert.Equal(t, namespace, config.Namespace()) } +func TestSaveTimeout(t *testing.T) { + dir, cleanup := tmpDir(t) + defer cleanup() + + // Set flags + flags := pflag.NewFlagSet("config-dir", pflag.ContinueOnError) + flags.String("config-dir", dir, "") + + config := Load(flags) + + timeout := 30 * time.Second + require.NoError(t, config.SaveTimeout(timeout)) + assert.Equal(t, timeout, config.Timeout()) +} + func TestSaveTokens(t *testing.T) { dir, cleanup := tmpDir(t) defer cleanup() diff --git a/cli/client/config/config.go b/cli/client/config/config.go index 29c9079710..81f3a75886 100644 --- a/cli/client/config/config.go +++ b/cli/client/config/config.go @@ -1,6 +1,8 @@ package config import ( + "time" + "github.com/sensu/sensu-go/types" ) @@ -11,6 +13,9 @@ const ( // DefaultFormat is the default format output for printers. DefaultFormat = FormatTabular + // DefaultTimeout is the default timeout + DefaultTimeout = 15 * time.Second + // FormatTabular indicates tabular format for printers. FormatTabular = "tabular" @@ -38,6 +43,7 @@ type Read interface { InsecureSkipTLSVerify() bool Namespace() string Tokens() *types.Tokens + Timeout() time.Duration TrustedCAFile() string } @@ -49,4 +55,5 @@ type Write interface { SaveNamespace(string) error SaveTokens(*types.Tokens) error SaveTrustedCAFile(string) error + SaveTimeout(time.Duration) error } diff --git a/cli/client/config/inmemory/inmemory.go b/cli/client/config/inmemory/inmemory.go index 91de9e5943..388eca92e2 100644 --- a/cli/client/config/inmemory/inmemory.go +++ b/cli/client/config/inmemory/inmemory.go @@ -1,6 +1,8 @@ package inmemory import ( + "time" + "github.com/sensu/sensu-go/cli/client/config" "github.com/sensu/sensu-go/types" ) @@ -10,6 +12,7 @@ type Config struct { url string format string namespace string + timeout time.Duration tokens *types.Tokens } @@ -39,6 +42,11 @@ func (c *Config) Namespace() string { return c.namespace } +// Timeout describes the timeout for communicating with the backend +func (c *Config) Timeout() time.Duration { + return c.timeout +} + // Tokens describes the authorization tokens used to make requests func (c *Config) Tokens() *types.Tokens { return c.tokens @@ -62,6 +70,12 @@ func (c *Config) SaveNamespace(val string) error { return nil } +// SaveTimeout updates the current timeout value +func (c *Config) SaveTimeout(val time.Duration) error { + c.timeout = val + return nil +} + // SaveTokens updates the current value func (c *Config) SaveTokens(val *types.Tokens) error { c.tokens = val diff --git a/cli/client/config/mock.go b/cli/client/config/mock.go index f3fc7da8eb..d0fe3cef75 100644 --- a/cli/client/config/mock.go +++ b/cli/client/config/mock.go @@ -1,6 +1,8 @@ package config import ( + "time" + corev2 "github.com/sensu/sensu-go/api/core/v2" "github.com/stretchr/testify/mock" @@ -38,6 +40,12 @@ func (m *MockConfig) Namespace() string { return args.String(0) } +// Timeout mocks the timeout config +func (m *MockConfig) Timeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + // TrustedCAFile mocks the trusted CA file config func (m *MockConfig) TrustedCAFile() string { args := m.Called() @@ -68,6 +76,12 @@ func (m *MockConfig) SaveNamespace(namespace string) error { return args.Error(0) } +// SaveTimeout mocks saving the timeout +func (m *MockConfig) SaveTimeout(timeout time.Duration) error { + args := m.Called(timeout) + return args.Error(0) +} + // SaveTokens mocks saving the tokens func (m *MockConfig) SaveTokens(tokens *corev2.Tokens) error { args := m.Called(tokens) diff --git a/cli/client/testing/mock_config.go b/cli/client/testing/mock_config.go index 4949bd30e6..745bd830d4 100644 --- a/cli/client/testing/mock_config.go +++ b/cli/client/testing/mock_config.go @@ -1,6 +1,8 @@ package testing import ( + "time" + "github.com/sensu/sensu-go/types" "github.com/stretchr/testify/mock" ) @@ -43,6 +45,12 @@ func (m *MockConfig) TrustedCAFile() string { return args.String(0) } +// Timeout mocks the timeout config +func (m *MockConfig) Timeout() time.Duration { + args := m.Called() + return args.Get(0).(time.Duration) +} + // SaveAPIUrl mocks saving the API URL func (m *MockConfig) SaveAPIUrl(url string) error { args := m.Called(url) @@ -67,6 +75,12 @@ func (m *MockConfig) SaveNamespace(namespace string) error { return args.Error(0) } +// SaveTimeout mocks saving the timeout +func (m *MockConfig) SaveTimeout(timeout time.Duration) error { + args := m.Called(timeout) + return args.Error(0) +} + // SaveTokens mocks saving the tokens func (m *MockConfig) SaveTokens(tokens *types.Tokens) error { args := m.Called(tokens) diff --git a/cli/commands/config/help.go b/cli/commands/config/help.go index 0ec4fd6564..e9ee2a3335 100644 --- a/cli/commands/config/help.go +++ b/cli/commands/config/help.go @@ -16,6 +16,7 @@ func HelpCommand(cli *cli.SensuCli) *cobra.Command { cmd.AddCommand( SetFormatCommand(cli), SetNamespaceCommand(cli), + SetTimeoutCommand(cli), ViewCommand(cli), ) diff --git a/cli/commands/config/set_timeout.go b/cli/commands/config/set_timeout.go new file mode 100644 index 0000000000..b8d1f135be --- /dev/null +++ b/cli/commands/config/set_timeout.go @@ -0,0 +1,52 @@ +package config + +import ( + "errors" + "fmt" + "time" + + "github.com/sensu/sensu-go/cli" + "github.com/sensu/sensu-go/cli/commands/hooks" + "github.com/spf13/cobra" +) + +// SetTimeoutCommand given argument changes timeout for active profile +func SetTimeoutCommand(cli *cli.SensuCli) *cobra.Command { + return &cobra.Command{ + Use: "set-timeout [TIMEOUT]", + Short: "Set timeout for active profile in duration format (ex: 15s)", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + _ = cmd.Help() + return errors.New("invalid argument(s) received") + } + + newTimeout := args[0] + newTimeoutDuration, err := time.ParseDuration(newTimeout) + if err != nil { + fmt.Fprintf( + cmd.OutOrStderr(), + "Unable to parse new timeout with error: %s\n", + err, + ) + return err + } + if err := cli.Config.SaveTimeout(newTimeoutDuration); err != nil { + fmt.Fprintf( + cmd.OutOrStderr(), + "Unable to write new configuration file with error: %s\n", + err, + ) + } + + fmt.Fprintln(cmd.OutOrStdout(), "Updated") + return nil + }, + Annotations: map[string]string{ + // We want to be able to run this command regardless of whether the CLI + // has been configured. + hooks.ConfigurationRequirement: hooks.ConfigurationNotRequired, + }, + } +} diff --git a/cli/commands/config/set_timeout_test.go b/cli/commands/config/set_timeout_test.go new file mode 100644 index 0000000000..85504f1bde --- /dev/null +++ b/cli/commands/config/set_timeout_test.go @@ -0,0 +1,69 @@ +package config + +import ( + "errors" + "testing" + "time" + + "github.com/sensu/sensu-go/cli" + clienttest "github.com/sensu/sensu-go/cli/client/testing" + test "github.com/sensu/sensu-go/cli/commands/testing" + "github.com/stretchr/testify/assert" +) + +func TestSetTimeoutCommand(t *testing.T) { + assert := assert.New(t) + + cli := &cli.SensuCli{} + cmd := SetTimeoutCommand(cli) + + assert.NotNil(cmd, "cmd should be returned") + assert.NotNil(cmd.RunE, "cmd should be able to be executed") + assert.Regexp("set-timeout", cmd.Use) + assert.Regexp("Set timeout", cmd.Short) +} + +func TestSetTimeoutBadsArgs(t *testing.T) { + assert := assert.New(t) + + cli := &cli.SensuCli{} + cmd := SetTimeoutCommand(cli) + + // No args... + out, err := test.RunCmd(cmd, []string{}) + assert.NotEmpty(out, "output should display help usage") + assert.Error(err, "error should be returned") + + // Too many args... + out, err = test.RunCmd(cmd, []string{"one", "two"}) + assert.NotEmpty(out, "output should display help usage") + assert.Error(err, "error should be returned") +} + +func TestSetTimeoutExec(t *testing.T) { + assert := assert.New(t) + + cli := test.NewMockCLI() + cmd := SetTimeoutCommand(cli) + + config := cli.Config.(*clienttest.MockConfig) + config.On("SaveTimeout", 15*time.Second).Return(nil) + + out, err := test.RunCmd(cmd, []string{"15s"}) + assert.Equal(out, "Updated\n") + assert.Nil(err, "Should not produce any errors") +} + +func TestSetTimeoutWithWriteErr(t *testing.T) { + assert := assert.New(t) + + cli := test.NewMockCLI() + cmd := SetTimeoutCommand(cli) + + config := cli.Config.(*clienttest.MockConfig) + config.On("SaveTimeout", 15*time.Second).Return(errors.New("blah")) + + out, err := test.RunCmd(cmd, []string{"15s"}) + assert.Contains(out, "Unable to write") + assert.Nil(err, "Should not return an error") +} diff --git a/cli/commands/config/view.go b/cli/commands/config/view.go index 4f05c9c11b..7e1463cd96 100644 --- a/cli/commands/config/view.go +++ b/cli/commands/config/view.go @@ -28,6 +28,7 @@ func ViewCommand(cli *cli.SensuCli) *cobra.Command { "api-url": cli.Config.APIUrl(), "namespace": cli.Config.Namespace(), "format": cli.Config.Format(), + "timeout": cli.Config.Timeout().String(), "username": helpers.GetCurrentUsername(cli.Config), "jwt_expires_at": strconv.Itoa(int(cli.Config.Tokens().GetExpiresAt())), } @@ -70,6 +71,10 @@ func printToList(v interface{}, writer io.Writer) error { Label: "Format", Value: r["format"], }, + { + Label: "Timeout", + Value: r["timeout"], + }, { Label: "Username", Value: r["username"], diff --git a/cli/commands/config/view_test.go b/cli/commands/config/view_test.go index 01703cdd48..12ed9082bf 100644 --- a/cli/commands/config/view_test.go +++ b/cli/commands/config/view_test.go @@ -2,6 +2,7 @@ package config import ( "testing" + "time" corev2 "github.com/sensu/sensu-go/api/core/v2" clienttest "github.com/sensu/sensu-go/cli/client/testing" @@ -37,6 +38,7 @@ func TestViewExec(t *testing.T) { config.On("Tokens").Return( corev2.FixtureTokens("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjIxODkzMTcsImp0aSI6IjAwZDFlYTE2OGU1MTQ1ZGEzN2U2Njg0YmRlOTgwNDM4Iiwic3ViIjoiYWRtaW4iLCJncm91cHMiOlsiY2x1c3Rlci1hZG1pbnMiLCJzeXN0ZW06dXNlcnMiXSwicHJvdmlkZXIiOnsicHJvdmlkZXJfaWQiOiJiYXNpYyIsInVzZXJfaWQiOiJhZG1pbiJ9fQ.ksuMGCJtkN5724CQ7e2W1P7T2ZPpR8IxU3fH9WhBMLk", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0MGVhYTRiMzRkMzU4YTkzNTY5YzIzZWM1YjcxNmZiMiIsInN1YiI6ImFkbWluIiwiZ3JvdXBzIjpudWxsLCJwcm92aWRlciI6eyJwcm92aWRlcl9pZCI6IiIsInVzZXJfaWQiOiIifX0.7t0qoBvKEkHD1DJbhP-VfSj95yhsFyrPoeFhqEbKOn8"), ) + config.On("Timeout").Return(time.Second * 15) out, err := test.RunCmd(cmd, nil) assert.Regexp("Active Configuration", out) diff --git a/cli/commands/configure/configure.go b/cli/commands/configure/configure.go index cd910be9e9..bbd6c62084 100644 --- a/cli/commands/configure/configure.go +++ b/cli/commands/configure/configure.go @@ -3,6 +3,7 @@ package configure import ( "errors" "fmt" + "time" "github.com/AlecAivazis/survey" "github.com/sensu/sensu-go/cli" @@ -19,6 +20,7 @@ type configureAnswers struct { Format string `survey:"format"` Namespace string `survey:"namespace"` InsecureSkipTLSVerify bool + Timeout time.Duration TrustedCAFile string } @@ -127,6 +129,24 @@ func Command(cli *cli.SensuCli) *cobra.Command { } } + if value, err := flags.GetString("timeout"); err == nil { + duration, err := time.ParseDuration(value) + if err != nil { + fmt.Fprintln(cmd.OutOrStderr()) + return fmt.Errorf( + "unable to parse timeout with error: %s", + err, + ) + } + if err = cli.Config.SaveTimeout(duration); err != nil { + fmt.Fprintln(cmd.OutOrStderr()) + return fmt.Errorf( + "unable to write new configuration file with error: %s", + err, + ) + } + } + return nil }, Annotations: map[string]string{ @@ -142,6 +162,7 @@ func Command(cli *cli.SensuCli) *cobra.Command { _ = cmd.Flags().StringP("password", "", "", "password") _ = cmd.Flags().StringP("format", "", cli.Config.Format(), "preferred output format") _ = cmd.Flags().StringP("namespace", "", cli.Config.Namespace(), "namespace") + _ = cmd.Flags().DurationP("timeout", "", cli.Config.Timeout(), "timeout when communicating with backend url") return cmd } @@ -164,6 +185,7 @@ func (answers *configureAnswers) withFlags(flags *pflag.FlagSet) { answers.Password, _ = flags.GetString("password") answers.Format, _ = flags.GetString("format") answers.Namespace, _ = flags.GetString("namespace") + answers.Timeout, _ = flags.GetDuration("timeout") } func askForURL(c config.Config) *survey.Question { diff --git a/cli/commands/configure/configure_test.go b/cli/commands/configure/configure_test.go index 1cf90072b7..e8ebba8d27 100644 --- a/cli/commands/configure/configure_test.go +++ b/cli/commands/configure/configure_test.go @@ -3,6 +3,7 @@ package configure import ( "bytes" "testing" + "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -23,6 +24,7 @@ func TestCommand(t *testing.T) { mockConfig := cli.Config.(*client.MockConfig) mockConfig.On("Format").Return(config.DefaultFormat) mockConfig.On("APIUrl").Return("http://127.0.0.1:8080") + mockConfig.On("Timeout").Return(time.Second * 15) cmd := Command(cli) @@ -48,6 +50,7 @@ func TestCommandRunEClosureWithFlags(t *testing.T) { mockConfig.On("SaveNamespace", mock.Anything).Return(nil) mockConfig.On("SaveInsecureSkipTLSVerify", mock.Anything).Return(nil) mockConfig.On("SaveTrustedCAFile", mock.Anything).Return(nil) + mockConfig.On("Timeout").Return(time.Second * 15) // We need to call the "configure" command via the rootCmd so the global flags // are set diff --git a/cli/commands/root/root.go b/cli/commands/root/root.go index c825e207e7..b93adc7781 100644 --- a/cli/commands/root/root.go +++ b/cli/commands/root/root.go @@ -1,6 +1,8 @@ package root import ( + "time" + "github.com/sensu/sensu-go/cli" "github.com/sensu/sensu-go/cli/client/config" "github.com/sensu/sensu-go/cli/commands/version" @@ -32,6 +34,7 @@ func Command() *cobra.Command { cmd.PersistentFlags().String("config-dir", path.UserConfigDir("sensuctl"), "path to directory containing configuration files") cmd.PersistentFlags().String("cache-dir", path.UserCacheDir("sensuctl"), "path to directory containing cache & temporary files") cmd.PersistentFlags().String("namespace", config.DefaultNamespace, "namespace in which we perform actions") + cmd.PersistentFlags().Duration("timeout", 15*time.Second, "timeout when communicating with sensu backend") return cmd }