diff --git a/README.md b/README.md index e2940ff7..b81f9214 100644 --- a/README.md +++ b/README.md @@ -407,7 +407,7 @@ You can now override the context portForward default address configuration by se bozo: bozo/gpu # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info) screenDumpDir: /tmp/dumps - # Represents ui poll intervals in seconds. Default 2secs + # Represents ui poll intervals in seconds. Default 2.0 secs. Minimum value is 2.0 - values below will be capped to the minimum. refreshRate: 2 # Overrides the default k8s api server requests timeout. Defaults 120s apiServerTimeout: 15s diff --git a/cmd/root.go b/cmd/root.go index 89760ba6..27c51b96 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -116,7 +116,7 @@ func run(*cobra.Command, []string) error { app.Config.SetActiveView(app.Config.K9s.DefaultView) } - if err := app.Init(version, *k9sFlags.RefreshRate); err != nil { + if err := app.Init(version, int(*k9sFlags.RefreshRate)); err != nil { return err } if err := app.Run(); err != nil { @@ -185,11 +185,11 @@ func parseLevel(level string) slog.Level { func initK9sFlags() { k9sFlags = config.NewFlags() - rootCmd.Flags().IntVarP( + rootCmd.Flags().Float32VarP( k9sFlags.RefreshRate, "refresh", "r", config.DefaultRefreshRate, - "Specify the default refresh rate as an integer (sec)", + "Specify the default refresh rate as a float (sec)", ) rootCmd.Flags().StringVarP( k9sFlags.LogLevel, diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2d9c479d..ceae3d9e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -544,7 +544,7 @@ func TestConfigLoad(t *testing.T) { cfg := mock.NewMockConfig(t) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) - assert.Equal(t, 2, cfg.K9s.RefreshRate) + assert.InDelta(t, 2.0, cfg.K9s.RefreshRate, 0.001) assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) } diff --git a/internal/config/flags.go b/internal/config/flags.go index 6c651217..51e7732c 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -5,7 +5,7 @@ package config const ( // DefaultRefreshRate represents the refresh interval. - DefaultRefreshRate = 2 // secs + DefaultRefreshRate float32 = 2.0 // secs // DefaultLogLevel represents the default log level. DefaultLogLevel = "info" @@ -16,7 +16,7 @@ const ( // Flags represents K9s configuration flags. type Flags struct { - RefreshRate *int + RefreshRate *float32 LogLevel *string LogFile *string Headless *bool @@ -33,7 +33,7 @@ type Flags struct { // NewFlags returns new configuration flags. func NewFlags() *Flags { return &Flags{ - RefreshRate: intPtr(DefaultRefreshRate), + RefreshRate: float32Ptr(DefaultRefreshRate), LogLevel: strPtr(DefaultLogLevel), LogFile: strPtr(AppLogFile), Headless: boolPtr(false), @@ -52,8 +52,8 @@ func boolPtr(b bool) *bool { return &b } -func intPtr(i int) *int { - return &i +func float32Ptr(f float32) *float32 { + return &f } func strPtr(s string) *string { diff --git a/internal/config/flags_test.go b/internal/config/flags_test.go index 1b51433f..bd5b2bf7 100644 --- a/internal/config/flags_test.go +++ b/internal/config/flags_test.go @@ -15,7 +15,7 @@ func TestNewFlags(t *testing.T) { config.AppLogFile = "/tmp/k9s-test/k9s.log" f := config.NewFlags() - assert.Equal(t, 2, *f.RefreshRate) + assert.InDelta(t, 2.0, *f.RefreshRate, 0.001) assert.Equal(t, "info", *f.LogLevel) assert.Equal(t, "/tmp/k9s-test/k9s.log", *f.LogFile) assert.Equal(t, config.AppDumpsDir, *f.ScreenDumpDir) diff --git a/internal/config/json/schemas/k9s.json b/internal/config/json/schemas/k9s.json index e1f7954f..8c5242b6 100644 --- a/internal/config/json/schemas/k9s.json +++ b/internal/config/json/schemas/k9s.json @@ -20,7 +20,7 @@ } }, "screenDumpDir": {"type": "string"}, - "refreshRate": { "type": "integer" }, + "refreshRate": { "type": "number" }, "apiServerTimeout": { "type": "string" }, "maxConnRetry": { "type": "integer" }, "readOnly": { "type": "boolean" }, diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 3c77b5eb..fc186585 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "sync" + "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config/data" @@ -35,7 +36,7 @@ type K9s struct { LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"` GPUVendors gpuVendors `json:"gpuVendors" yaml:"gpuVendors"` ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"` - RefreshRate int `json:"refreshRate" yaml:"refreshRate"` + RefreshRate float32 `json:"refreshRate" yaml:"refreshRate"` APIServerTimeout string `json:"apiServerTimeout" yaml:"apiServerTimeout"` MaxConnRetry int32 `json:"maxConnRetry" yaml:"maxConnRetry"` ReadOnly bool `json:"readOnly" yaml:"readOnly"` @@ -49,10 +50,11 @@ type K9s struct { Logger Logger `json:"logger" yaml:"logger"` Thresholds Threshold `json:"thresholds" yaml:"thresholds"` DefaultView string `json:"defaultView" yaml:"defaultView"` - manualRefreshRate int + manualRefreshRate float32 manualReadOnly *bool manualCommand *string manualScreenDumpDir *string + refreshRateWarned bool dir *data.Dir activeContextName string activeConfig *data.Config @@ -314,7 +316,7 @@ func (k *K9s) Reload() error { // Override overrides k9s config from cli args. func (k *K9s) Override(k9sFlags *Flags) { if k9sFlags.RefreshRate != nil && *k9sFlags.RefreshRate != DefaultRefreshRate { - k.manualRefreshRate = *k9sFlags.RefreshRate + k.manualRefreshRate = float32(*k9sFlags.RefreshRate) } k.UI.manualHeadless = k9sFlags.Headless @@ -369,12 +371,29 @@ func (k *K9s) IsSplashless() bool { } // GetRefreshRate returns the current refresh rate. -func (k *K9s) GetRefreshRate() int { - if k.manualRefreshRate != 0 { - return k.manualRefreshRate - } +func (k *K9s) GetRefreshRate() float32 { + k.mx.Lock() + defer k.mx.Unlock() - return k.RefreshRate + rate := k.RefreshRate + if k.manualRefreshRate != 0 { + rate = k.manualRefreshRate + } + if rate < DefaultRefreshRate { + if !k.refreshRateWarned { + slog.Warn("Refresh rate is below minimum, capping to minimum value", + slogs.Requested, float64(rate), + slogs.Minimum, float64(DefaultRefreshRate)) + k.refreshRateWarned = true + } + return DefaultRefreshRate + } + return rate +} + +// RefreshDuration returns the refresh rate as a time.Duration. +func (k *K9s) RefreshDuration() time.Duration { + return time.Duration(k.GetRefreshRate() * float32(time.Second)) } // IsReadOnly returns the readonly setting. diff --git a/internal/config/k9s_int_test.go b/internal/config/k9s_int_test.go index a2b18ec5..fd51b15e 100644 --- a/internal/config/k9s_int_test.go +++ b/internal/config/k9s_int_test.go @@ -19,14 +19,14 @@ func Test_k9sOverrides(t *testing.T) { uu := map[string]struct { k *K9s - rate int + rate float32 ro, hl, cl, sl, ll bool }{ "plain": { k: &K9s{ LiveViewAutoRefresh: false, ScreenDumpDir: "", - RefreshRate: 10, + RefreshRate: 10.0, MaxConnRetry: 0, ReadOnly: false, NoExitOnCtrlC: false, @@ -34,13 +34,27 @@ func Test_k9sOverrides(t *testing.T) { SkipLatestRevCheck: false, DisablePodCounting: false, }, - rate: 10, + rate: 10.0, + }, + "sub-second": { + k: &K9s{ + LiveViewAutoRefresh: false, + ScreenDumpDir: "", + RefreshRate: 0.5, + MaxConnRetry: 0, + ReadOnly: false, + NoExitOnCtrlC: false, + UI: UI{}, + SkipLatestRevCheck: false, + DisablePodCounting: false, + }, + rate: 2.0, // minimum enforced }, "set": { k: &K9s{ LiveViewAutoRefresh: false, ScreenDumpDir: "", - RefreshRate: 10, + RefreshRate: 10.0, MaxConnRetry: 0, ReadOnly: true, NoExitOnCtrlC: false, @@ -53,7 +67,7 @@ func Test_k9sOverrides(t *testing.T) { SkipLatestRevCheck: false, DisablePodCounting: false, }, - rate: 10, + rate: 10.0, ro: true, hl: true, ll: true, @@ -64,7 +78,7 @@ func Test_k9sOverrides(t *testing.T) { k: &K9s{ LiveViewAutoRefresh: false, ScreenDumpDir: "", - RefreshRate: 10, + RefreshRate: 10.0, MaxConnRetry: 0, ReadOnly: false, NoExitOnCtrlC: false, @@ -79,12 +93,12 @@ func Test_k9sOverrides(t *testing.T) { }, SkipLatestRevCheck: false, DisablePodCounting: false, - manualRefreshRate: 100, + manualRefreshRate: 100.0, manualReadOnly: &trueVal, manualCommand: &cmd, manualScreenDumpDir: &dir, }, - rate: 100, + rate: 100.0, ro: true, hl: true, ll: true, @@ -96,7 +110,7 @@ func Test_k9sOverrides(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.rate, u.k.GetRefreshRate()) + assert.InDelta(t, u.rate, u.k.GetRefreshRate(), 0.001) assert.Equal(t, u.ro, u.k.IsReadOnly()) assert.Equal(t, u.cl, u.k.IsCrumbsless()) assert.Equal(t, u.sl, u.k.IsSplashless()) diff --git a/internal/config/refresh_rate_test.go b/internal/config/refresh_rate_test.go new file mode 100644 index 00000000..72322f14 --- /dev/null +++ b/internal/config/refresh_rate_test.go @@ -0,0 +1,75 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestRefreshRateBackwardCompatibility(t *testing.T) { + tests := map[string]struct { + yamlContent string + expected float32 + }{ + "integer_value": { + yamlContent: `refreshRate: 2`, + expected: 2.0, + }, + "float_value": { + yamlContent: `refreshRate: 2.5`, + expected: 2.5, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var k K9s + err := yaml.Unmarshal([]byte(test.yamlContent), &k) + require.NoError(t, err) + assert.InDelta(t, test.expected, k.RefreshRate, 0.001) + }) + } +} + +func TestGetRefreshRateMinimum(t *testing.T) { + tests := map[string]struct { + refreshRate float32 + manualRefreshRate float32 + expected float32 + }{ + "below_minimum": { + refreshRate: 0.5, + expected: 2.0, + }, + "at_minimum": { + refreshRate: 2.0, + expected: 2.0, + }, + "above_minimum": { + refreshRate: 3.5, + expected: 3.5, + }, + "manual_below_minimum": { + refreshRate: 3.0, + manualRefreshRate: 0.5, + expected: 2.0, + }, + "manual_above_minimum": { + refreshRate: 2.0, + manualRefreshRate: 4.0, + expected: 4.0, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + k := K9s{ + RefreshRate: test.refreshRate, + manualRefreshRate: test.manualRefreshRate, + } + assert.InDelta(t, test.expected, k.GetRefreshRate(), 0.001) + }) + } +} diff --git a/internal/slogs/keys.go b/internal/slogs/keys.go index 14a0477a..f36ba0de 100644 --- a/internal/slogs/keys.go +++ b/internal/slogs/keys.go @@ -221,4 +221,10 @@ const ( // Type tracks a type logger key. Type = "type" + + // Requested tracks a requested value logger key. + Requested = "requested" + + // Minimum tracks a minimum value logger key. + Minimum = "minimum" ) diff --git a/internal/view/browser.go b/internal/view/browser.go index 0a98dc6d..237b15ef 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -112,7 +112,7 @@ func (b *Browser) Init(ctx context.Context) error { if row == 0 && b.GetRowCount() > 0 { b.Select(1, 0) } - b.GetModel().SetRefreshRate(time.Duration(b.App().Config.K9s.GetRefreshRate()) * time.Second) + b.GetModel().SetRefreshRate(b.App().Config.K9s.RefreshDuration()) b.CmdBuff().SetSuggestionFn(b.suggestFilter()) diff --git a/internal/view/table.go b/internal/view/table.go index 2312bd27..9e89a961 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -8,7 +8,6 @@ import ( "log/slog" "path/filepath" "strings" - "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" @@ -60,7 +59,7 @@ func (t *Table) Init(ctx context.Context) (err error) { } t.SetInputCapture(t.keyboard) t.bindKeys() - t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second) + t.GetModel().SetRefreshRate(t.app.Config.K9s.RefreshDuration()) t.CmdBuff().AddListener(t) return nil diff --git a/internal/view/xray.go b/internal/view/xray.go index 048b11a4..15ad0ffe 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -9,7 +9,6 @@ import ( "log/slog" "regexp" "strings" - "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" @@ -87,7 +86,7 @@ func (x *Xray) Init(ctx context.Context) error { x.SetGraphicsColor(x.app.Styles.Xray().GraphicColor.Color()) x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, cases.Title(language.Und, cases.NoLower).String(x.gvr.R()))) - x.model.SetRefreshRate(time.Duration(x.app.Config.K9s.GetRefreshRate()) * time.Second) + x.model.SetRefreshRate(x.app.Config.K9s.RefreshDuration()) x.model.SetNamespace(client.CleanseNamespace(x.app.Config.ActiveNamespace())) x.model.AddListener(x)