From f682a02ca927fccc1b9ba0f2724115bd46310c4e Mon Sep 17 00:00:00 2001 From: Jan-Kees van Andel Date: Wed, 20 Oct 2021 16:01:51 +0200 Subject: [PATCH 1/6] Some small color modifications (#1286) Because in some situations, the text became invisible --- skins/red.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skins/red.yml b/skins/red.yml index 5a89e677..5ace7c5f 100644 --- a/skins/red.yml +++ b/skins/red.yml @@ -29,16 +29,16 @@ k9s: numKeyColor: red crumbs: fgColor: black - bgColor: steelblue + bgColor: red activeColor: red status: - newColor: lightskyblue + newColor: red modifyColor: greenyellow addColor: white - errorColor: redred + errorColor: red pendingColor: darkred highlightcolor: red - killColor: mediumpurple + killColor: red completedColor: gray title: fgColor: red @@ -55,7 +55,7 @@ k9s: - linegreen - redred table: - fgColor: blue + fgColor: red bgColor: black cursorFgColor: black cursorBgColor: red @@ -65,15 +65,15 @@ k9s: bgColor: black sorterColor: red xray: - fgColor: blue + fgColor: red bgColor: black cursorColor: red graphicColor: darkgoldenrod showIcons: false yaml: - keyColor: steelblue + keyColor: red colonColor: white - valueColor: papayawhip + valueColor: red logs: fgColor: white bgColor: black From 72b466d5ffecb9481dcba29b50eb2a00c1405537 Mon Sep 17 00:00:00 2001 From: Vladimir Varankin Date: Wed, 20 Oct 2021 16:04:50 +0200 Subject: [PATCH 2/6] skins: add missing directive to kiss (#1287) --- skins/kiss.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skins/kiss.yml b/skins/kiss.yml index c65829c4..aeffa226 100644 --- a/skins/kiss.yml +++ b/skins/kiss.yml @@ -63,3 +63,6 @@ k9s: logs: fgColor: default bgColor: default + indicator: + fgColor: default + bgColor: default From 3b28b94bc79df1f15998b7c95387e7c77cac216c Mon Sep 17 00:00:00 2001 From: Rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Thu, 21 Oct 2021 12:23:15 +0800 Subject: [PATCH 3/6] Fix a small typo which comes from the cluster info view (#1284) --- internal/model/cluster_info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 05cdeb38..f34645d6 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -116,7 +116,7 @@ func (c *ClusterInfo) Reset(f dao.Factory) { c.Refresh() } -// Refresh fetches latest cluster meta. +// Refresh fetches the latest cluster meta. func (c *ClusterInfo) Refresh() { data := NewClusterMeta() data.Context = c.cluster.ContextName() From 8d47b3dceaf55b3f432a9aa93906d2c24c8ad788 Mon Sep 17 00:00:00 2001 From: Zach Peters Date: Wed, 20 Oct 2021 23:39:23 -0500 Subject: [PATCH 4/6] Removed cusor colors that are too light to read (#1271) --- skins/dracula.yml | 2 -- skins/monokai.yml | 2 -- skins/nord.yml | 2 -- skins/snazzy.yml | 2 -- 4 files changed, 8 deletions(-) diff --git a/skins/dracula.yml b/skins/dracula.yml index 38d63278..78253093 100644 --- a/skins/dracula.yml +++ b/skins/dracula.yml @@ -83,8 +83,6 @@ k9s: table: fgColor: *foreground bgColor: *background - cursorFgColor: *foreground - cursorBgColor: *current_line # Header row styles. header: fgColor: *foreground diff --git a/skins/monokai.yml b/skins/monokai.yml index 04605bc6..69cf2709 100644 --- a/skins/monokai.yml +++ b/skins/monokai.yml @@ -104,8 +104,6 @@ k9s: table: fgColor: *foreground bgColor: *background - cursorFgColor: *foreground - cursorBgColor: *backgroundOpaque markColor: *magenta # Header row styles. header: diff --git a/skins/nord.yml b/skins/nord.yml index d481159d..e8e73a5f 100644 --- a/skins/nord.yml +++ b/skins/nord.yml @@ -81,8 +81,6 @@ k9s: table: fgColor: *foreground bgColor: default - cursorFgColor: *foreground - cursorBgColor: *current_line # Header row styles. header: fgColor: *foreground diff --git a/skins/snazzy.yml b/skins/snazzy.yml index b101007a..f6ebf2cb 100644 --- a/skins/snazzy.yml +++ b/skins/snazzy.yml @@ -58,8 +58,6 @@ k9s: table: fgColor: "#57c7ff" bgColor: "#282a36" - cursorFgColor: "#57c7ff" - cursorBgColor: "#5af78e" markColor: darkgoldenrod header: fgColor: white From 5166adb7c1ccc269aef22714f52ab7cab4ab235c Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Mon, 8 Nov 2021 23:47:18 +0800 Subject: [PATCH 5/6] refactor: move from io/ioutil to io and os packages (#1300) The io/ioutil package has been deprecated as of Go 1.16, see https://golang.org/doc/go1.16#ioutil. This commit replaces the existing io/ioutil functions with their new definitions in io and os packages. Signed-off-by: Eng Zer Jun --- internal/config/alias.go | 6 +++--- internal/config/bench.go | 4 ++-- internal/config/config.go | 5 ++--- internal/config/config_test.go | 6 +++--- internal/config/hotkey.go | 4 ++-- internal/config/plugin.go | 4 ++-- internal/config/styles.go | 4 ++-- internal/config/views.go | 4 ++-- internal/dao/benchmark.go | 8 +++++--- internal/dao/cruiser_test.go | 4 ++-- internal/dao/dir.go | 8 ++++---- internal/dao/ofaas.go | 4 ++-- internal/dao/registry_test.go | 4 ++-- internal/dao/screen_dump.go | 7 ++++--- internal/model/cluster_info.go | 4 ++-- internal/model/table_int_test.go | 8 ++++---- internal/model/table_test.go | 4 ++-- internal/perf/benchmark.go | 3 +-- internal/render/benchmark.go | 3 +-- internal/render/benchmark_int_test.go | 4 ++-- internal/render/dir.go | 8 ++++---- internal/render/render_test.go | 4 ++-- internal/view/benchmark.go | 4 ++-- internal/view/dir.go | 8 ++++---- internal/view/log_test.go | 6 +++--- internal/view/table_int_test.go | 6 +++--- internal/xray/container_test.go | 4 ++-- 27 files changed, 69 insertions(+), 69 deletions(-) diff --git a/internal/config/alias.go b/internal/config/alias.go index a06b0c7f..8ded5654 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -1,7 +1,7 @@ package config import ( - "io/ioutil" + "os" "path/filepath" "sync" @@ -105,7 +105,7 @@ func (a *Aliases) Load() error { // LoadFileAliases loads alias from a given file. func (a *Aliases) LoadFileAliases(path string) error { - f, err := ioutil.ReadFile(path) + f, err := os.ReadFile(path) if err == nil { var aa Aliases if err := yaml.Unmarshal(f, &aa); err != nil { @@ -171,5 +171,5 @@ func (a *Aliases) SaveAliases(path string) error { if err != nil { return err } - return ioutil.WriteFile(path, cfg, 0644) + return os.WriteFile(path, cfg, 0644) } diff --git a/internal/config/bench.go b/internal/config/bench.go index 40999d6d..c3f6c4c9 100644 --- a/internal/config/bench.go +++ b/internal/config/bench.go @@ -1,8 +1,8 @@ package config import ( - "io/ioutil" "net/http" + "os" "gopkg.in/yaml.v2" ) @@ -96,7 +96,7 @@ func (s *Bench) Reload(path string) error { // Load K9s benchmark configs from file. func (s *Bench) load(path string) error { - f, err := ioutil.ReadFile(path) + f, err := os.ReadFile(path) if err != nil { return err } diff --git a/internal/config/config.go b/internal/config/config.go index 0118da7f..90f7c73f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,7 +3,6 @@ package config import ( "errors" "fmt" - "io/ioutil" "os" "path/filepath" @@ -218,7 +217,7 @@ func (c *Config) SetConnection(conn client.Connection) { // Load K9s configuration from file. func (c *Config) Load(path string) error { - f, err := ioutil.ReadFile(path) + f, err := os.ReadFile(path) if err != nil { return err } @@ -252,7 +251,7 @@ func (c *Config) SaveFile(path string) error { log.Error().Msgf("[Config] Unable to save K9s config file: %v", err) return err } - return ioutil.WriteFile(path, cfg, 0644) + return os.WriteFile(path, cfg, 0644) } // Validate the configuration. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index eb6ec179..55f37e4d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2,7 +2,7 @@ package config_test import ( "fmt" - "io/ioutil" + "os" "path/filepath" "testing" @@ -216,7 +216,7 @@ func TestConfigSaveFile(t *testing.T) { err := cfg.SaveFile(path) assert.Nil(t, err) - raw, err := ioutil.ReadFile(path) + raw, err := os.ReadFile(path) assert.Nil(t, err) assert.Equal(t, expectedConfig, string(raw)) } @@ -242,7 +242,7 @@ func TestConfigReset(t *testing.T) { err := cfg.SaveFile(path) assert.Nil(t, err) - raw, err := ioutil.ReadFile(path) + raw, err := os.ReadFile(path) assert.Nil(t, err) assert.Equal(t, resetConfig, string(raw)) } diff --git a/internal/config/hotkey.go b/internal/config/hotkey.go index 5a6988ea..707bc0c7 100644 --- a/internal/config/hotkey.go +++ b/internal/config/hotkey.go @@ -1,7 +1,7 @@ package config import ( - "io/ioutil" + "os" "path/filepath" "gopkg.in/yaml.v2" @@ -36,7 +36,7 @@ func (h HotKeys) Load() error { // LoadHotKeys loads plugins from a given file. func (h HotKeys) LoadHotKeys(path string) error { - f, err := ioutil.ReadFile(path) + f, err := os.ReadFile(path) if err != nil { return err } diff --git a/internal/config/plugin.go b/internal/config/plugin.go index f4e3a0a4..8cb8f48a 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -1,7 +1,7 @@ package config import ( - "io/ioutil" + "os" "path/filepath" "gopkg.in/yaml.v2" @@ -40,7 +40,7 @@ func (p Plugins) Load() error { // LoadPlugins loads plugins from a given file. func (p Plugins) LoadPlugins(path string) error { - f, err := ioutil.ReadFile(path) + f, err := os.ReadFile(path) if err != nil { return err } diff --git a/internal/config/styles.go b/internal/config/styles.go index e1ca257b..4a856e39 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -2,7 +2,7 @@ package config import ( "fmt" - "io/ioutil" + "os" "path/filepath" "github.com/derailed/tview" @@ -541,7 +541,7 @@ func (s *Styles) Views() Views { // Load K9s configuration from file. func (s *Styles) Load(path string) error { - f, err := ioutil.ReadFile(path) + f, err := os.ReadFile(path) if err != nil { return err } diff --git a/internal/config/views.go b/internal/config/views.go index 78664f1a..0d2a8407 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -1,7 +1,7 @@ package config import ( - "io/ioutil" + "os" "path/filepath" "gopkg.in/yaml.v2" @@ -56,7 +56,7 @@ func (v *CustomView) Reset() { // Load loads view configurations. func (v *CustomView) Load(path string) error { - raw, err := ioutil.ReadFile(path) + raw, err := os.ReadFile(path) if err != nil { return err } diff --git a/internal/dao/benchmark.go b/internal/dao/benchmark.go index 0c25af78..65f85a8e 100644 --- a/internal/dao/benchmark.go +++ b/internal/dao/benchmark.go @@ -3,7 +3,6 @@ package dao import ( "context" "errors" - "io/ioutil" "os" "path/filepath" "strings" @@ -41,7 +40,7 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error } path, _ := ctx.Value(internal.KeyPath).(string) - ff, err := ioutil.ReadDir(dir) + ff, err := os.ReadDir(dir) if err != nil { return nil, err } @@ -51,7 +50,10 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error if path != "" && !strings.HasPrefix(f.Name(), strings.Replace(path, "/", "_", 1)) { continue } - oo = append(oo, render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())}) + + if fi, err := f.Info(); err == nil { + oo = append(oo, render.BenchInfo{File: fi, Path: filepath.Join(dir, f.Name())}) + } } return oo, nil diff --git a/internal/dao/cruiser_test.go b/internal/dao/cruiser_test.go index 8574d8a4..da8769e7 100644 --- a/internal/dao/cruiser_test.go +++ b/internal/dao/cruiser_test.go @@ -3,7 +3,7 @@ package dao import ( "encoding/json" "fmt" - "io/ioutil" + "os" "testing" "github.com/stretchr/testify/assert" @@ -29,7 +29,7 @@ func TestCruiserSlice(t *testing.T) { // Helpers... func loadJSON(t assert.TestingT, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured diff --git a/internal/dao/dir.go b/internal/dao/dir.go index d395c365..b5a3a456 100644 --- a/internal/dao/dir.go +++ b/internal/dao/dir.go @@ -3,7 +3,7 @@ package dao import ( "context" "errors" - "io/ioutil" + "os" "path/filepath" "regexp" "strings" @@ -37,7 +37,7 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) { return nil, errors.New("No dir in context") } - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) if err != nil { return nil, err } @@ -48,8 +48,8 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) { continue } oo = append(oo, render.DirRes{ - Path: filepath.Join(dir, f.Name()), - Info: f, + Path: filepath.Join(dir, f.Name()), + Entry: f, }) } diff --git a/internal/dao/ofaas.go b/internal/dao/ofaas.go index 425d07aa..4716a484 100644 --- a/internal/dao/ofaas.go +++ b/internal/dao/ofaas.go @@ -8,7 +8,7 @@ package dao // "encoding/json" // "errors" // "fmt" -// "io/ioutil" +// "io" // "net/http" // "net/url" // "os" @@ -193,7 +193,7 @@ package dao // case http.StatusUnauthorized: // return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server") // default: -// bytesOut, err := ioutil.ReadAll(delRes.Body) +// bytesOut, err := io.ReadAll(delRes.Body) // if err != nil { // return err // } diff --git a/internal/dao/registry_test.go b/internal/dao/registry_test.go index 28ff4ef8..4d28895b 100644 --- a/internal/dao/registry_test.go +++ b/internal/dao/registry_test.go @@ -3,7 +3,7 @@ package dao import ( "encoding/json" "fmt" - "io/ioutil" + "os" "testing" "github.com/stretchr/testify/assert" @@ -89,7 +89,7 @@ func TestExtractString(t *testing.T) { // Helpers... func load(t *testing.T, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured diff --git a/internal/dao/screen_dump.go b/internal/dao/screen_dump.go index 09c0742b..644b958d 100644 --- a/internal/dao/screen_dump.go +++ b/internal/dao/screen_dump.go @@ -3,7 +3,6 @@ package dao import ( "context" "errors" - "io/ioutil" "os" "regexp" @@ -37,14 +36,16 @@ func (d *ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, erro return nil, errors.New("no screendump dir found in context") } - ff, err := ioutil.ReadDir(SanitizeFilename(dir)) + ff, err := os.ReadDir(SanitizeFilename(dir)) if err != nil { return nil, err } oo := make([]runtime.Object, len(ff)) for i, f := range ff { - oo[i] = render.FileRes{File: f, Dir: dir} + if fi, err := f.Info(); err == nil { + oo[i] = render.FileRes{File: fi, Dir: dir} + } } return oo, nil diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index f34645d6..57164e5d 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "time" @@ -200,7 +200,7 @@ func fetchLatestRev() (string, error) { } }() - b, err := ioutil.ReadAll(resp.Body) + b, err := io.ReadAll(resp.Body) if err != nil { return "", err } diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index 9247256c..8c6c5891 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "os" "testing" "github.com/derailed/k9s/internal" @@ -139,7 +139,7 @@ func TestTableGenericHydrate(t *testing.T) { // Helpers... func mustLoad(n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) if err != nil { panic(err) } @@ -151,7 +151,7 @@ func mustLoad(n string) *unstructured.Unstructured { } func load(t *testing.T, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured err = json.Unmarshal(raw, &o) @@ -160,7 +160,7 @@ func load(t *testing.T, n string) *unstructured.Unstructured { } func raw(t *testing.T, n string) []byte { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) return raw } diff --git a/internal/model/table_test.go b/internal/model/table_test.go index e41b584b..08d31f44 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "os" "testing" "github.com/derailed/k9s/internal" @@ -122,7 +122,7 @@ func makeTableFactory() tableFactory { } func mustLoad(n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) if err != nil { panic(err) } diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index f5ec8294..c5fd19d9 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "net/http" "os" "path/filepath" @@ -138,7 +137,7 @@ func (b *Benchmark) save(cluster string, r io.Reader) error { } }() - bb, err := ioutil.ReadAll(r) + bb, err := io.ReadAll(r) if err != nil { return err } diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go index 4b586262..710660ea 100644 --- a/internal/render/benchmark.go +++ b/internal/render/benchmark.go @@ -3,7 +3,6 @@ package render import ( "errors" "fmt" - "io/ioutil" "os" "regexp" "strconv" @@ -97,7 +96,7 @@ func (Benchmark) diagnose(ns string, ff Fields) error { // Helpers... func (Benchmark) readFile(file string) (string, error) { - data, err := ioutil.ReadFile(file) + data, err := os.ReadFile(file) if err != nil { return "", err } diff --git a/internal/render/benchmark_int_test.go b/internal/render/benchmark_int_test.go index 6c7c5384..4fe09639 100644 --- a/internal/render/benchmark_int_test.go +++ b/internal/render/benchmark_int_test.go @@ -1,7 +1,7 @@ package render import ( - "io/ioutil" + "os" "testing" "github.com/rs/zerolog" @@ -38,7 +38,7 @@ func TestAugmentRow(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - data, err := ioutil.ReadFile(u.file) + data, err := os.ReadFile(u.file) assert.Nil(t, err) fields := make(Fields, 8) diff --git a/internal/render/dir.go b/internal/render/dir.go index 12a3e941..d955fb98 100644 --- a/internal/render/dir.go +++ b/internal/render/dir.go @@ -35,10 +35,10 @@ func (Dir) Render(o interface{}, ns string, r *Row) error { } name := "🦄 " - if d.Info.IsDir() { + if d.Entry.IsDir() { name = "📁 " } - name += d.Info.Name() + name += d.Entry.Name() r.ID, r.Fields = d.Path, append(r.Fields, name) return nil @@ -49,8 +49,8 @@ func (Dir) Render(o interface{}, ns string, r *Row) error { // DirRes represents an alias resource. type DirRes struct { - Info os.FileInfo - Path string + Entry os.DirEntry + Path string } // GetObjectKind returns a schema object. diff --git a/internal/render/render_test.go b/internal/render/render_test.go index cd2f6492..20a1c331 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -3,7 +3,7 @@ package render_test import ( "encoding/json" "fmt" - "io/ioutil" + "os" "testing" "github.com/stretchr/testify/assert" @@ -13,7 +13,7 @@ import ( // Helpers... func load(t testing.TB, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index 892cdb23..f9b795d0 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -2,7 +2,7 @@ package view import ( "context" - "io/ioutil" + "os" "path/filepath" "strings" @@ -71,7 +71,7 @@ func benchDir(cfg *config.Config) string { } func readBenchFile(cfg *config.Config, n string) (string, error) { - data, err := ioutil.ReadFile(filepath.Join(benchDir(cfg), n)) + data, err := os.ReadFile(filepath.Join(benchDir(cfg), n)) if err != nil { return "", err } diff --git a/internal/view/dir.go b/internal/view/dir.go index 13c35d8b..c151714b 100644 --- a/internal/view/dir.go +++ b/internal/view/dir.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "io/ioutil" + "os" "path" "strings" @@ -90,7 +90,7 @@ func (d *Dir) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - yaml, err := ioutil.ReadFile(sel) + yaml, err := os.ReadFile(sel) if err != nil { d.App().Flash().Err(err) return nil @@ -157,7 +157,7 @@ func isKustomized(sel string) bool { return false } - ff, err := ioutil.ReadDir(sel) + ff, err := os.ReadDir(sel) if err != nil { return false } @@ -176,7 +176,7 @@ func containsDir(sel string) bool { return false } - ff, err := ioutil.ReadDir(sel) + ff, err := os.ReadDir(sel) if err != nil { return false } diff --git a/internal/view/log_test.go b/internal/view/log_test.go index c0454bb1..501f8e92 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -3,7 +3,7 @@ package view_test import ( "bytes" "fmt" - "io/ioutil" + "os" "path/filepath" "testing" @@ -79,9 +79,9 @@ func TestLogViewSave(t *testing.T) { v.Flush(ii.Lines(false)) config.K9sDumpDir = "/tmp" dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) - c1, _ := ioutil.ReadDir(dir) + c1, _ := os.ReadDir(dir) v.SaveCmd(nil) - c2, _ := ioutil.ReadDir(dir) + c2, _ := os.ReadDir(dir) assert.Equal(t, len(c2), len(c1)+1) } diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index 0ddab5fd..6209f194 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -2,7 +2,7 @@ package view import ( "context" - "io/ioutil" + "os" "path/filepath" "testing" "time" @@ -25,10 +25,10 @@ func TestTableSave(t *testing.T) { v.SetTitle("k9s-test") dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) - c1, _ := ioutil.ReadDir(dir) + c1, _ := os.ReadDir(dir) v.saveCmd(nil) - c2, _ := ioutil.ReadDir(dir) + c2, _ := os.ReadDir(dir) assert.Equal(t, len(c2), len(c1)+1) } diff --git a/internal/xray/container_test.go b/internal/xray/container_test.go index 4e66d0f8..d635525a 100644 --- a/internal/xray/container_test.go +++ b/internal/xray/container_test.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "os" "testing" "github.com/derailed/k9s/internal" @@ -238,7 +238,7 @@ func makeDoubleCMKeysContainer(n string, optional bool) *v1.Container { } func load(t *testing.T, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured From 4e20e5f110871e565e82b2569b94550ce1a3fe89 Mon Sep 17 00:00:00 2001 From: Fernand Galiana Date: Mon, 15 Nov 2021 23:24:51 -0700 Subject: [PATCH 6/6] K9s: Release v0.25.0 (#1304) * update rel notes * release 0.25 checkpoint * cleaning up --- Makefile | 2 +- README.md | 3 +- change_logs/release_v0.25.0.md | 121 +++++++++++++++ cmd/root.go | 26 ++-- go.mod | 6 +- go.sum | 5 +- internal/client/client.go | 19 +-- internal/client/config.go | 171 +++++++++------------ internal/client/config_test.go | 155 +++++++++++++------- internal/client/testdata/config | 73 ++++----- internal/client/testdata/config.1 | 24 +-- internal/client/tunnel.go | 11 -- internal/config/config.go | 28 +++- internal/config/config_test.go | 4 +- internal/config/plugin.go | 7 + internal/dao/context.go | 9 +- internal/dao/dp.go | 10 +- internal/dao/ds.go | 13 +- internal/dao/helm.go | 3 +- internal/dao/log_item.go | 94 ++++++------ internal/dao/log_item_test.go | 30 +++- internal/dao/log_items.go | 113 +++++++------- internal/dao/log_items_test.go | 14 +- internal/dao/log_options.go | 30 ++-- internal/dao/pod.go | 46 +++--- internal/dao/port_forwarder.go | 65 ++++---- internal/dao/registry.go | 5 +- internal/dao/sts.go | 4 +- internal/model/cluster_info.go | 41 +++--- internal/model/cmd_buff.go | 8 +- internal/model/flash.go | 4 +- internal/model/log.go | 212 +++++++++++++++++---------- internal/model/log_int_test.go | 15 +- internal/model/log_test.go | 44 ++++-- internal/model/table.go | 2 +- internal/port/ann.go | 20 +++ internal/port/ann_test.go | 93 ++++++++++++ internal/port/co_portspec.go | 147 +++++++++++++++++++ internal/port/co_portspec_test.go | 138 +++++++++++++++++ internal/port/pf.go | 102 +++++++++++++ internal/port/pf_test.go | 207 ++++++++++++++++++++++++++ internal/port/pfs.go | 92 ++++++++++++ internal/port/pfs_test.go | 185 +++++++++++++++++++++++ internal/port/tunnel.go | 49 +++++++ internal/port/tunnel_test.go | 32 ++++ internal/render/port_forward_test.go | 6 +- internal/render/portforward.go | 12 +- internal/ui/app.go | 8 +- internal/ui/config.go | 4 +- internal/ui/dialog/delete.go | 4 +- internal/ui/flash.go | 2 +- internal/ui/prompt.go | 15 +- internal/ui/prompt_test.go | 6 +- internal/view/actions.go | 1 + internal/view/app.go | 15 +- internal/view/app_test.go | 2 +- internal/view/browser.go | 19 ++- internal/view/cluster_info.go | 3 +- internal/view/container.go | 106 ++++++-------- internal/view/drain_dialog.go | 6 +- internal/view/exec.go | 100 ++++++++++--- internal/view/helpers.go | 2 +- internal/view/helpers_test.go | 8 +- internal/view/log.go | 176 +++++++++++++++++----- internal/view/log_indicator.go | 72 +++++---- internal/view/log_indicator_test.go | 21 ++- internal/view/log_int_test.go | 14 +- internal/view/log_test.go | 43 +++++- internal/view/logger.go | 3 +- internal/view/pf.go | 45 ++++-- internal/view/pf_dialog.go | 82 ++++------- internal/view/pf_dialog_test.go | 38 ----- internal/view/pf_extender.go | 85 ++++++----- internal/view/pod.go | 1 - internal/view/restart_extender.go | 2 +- internal/view/scale_extender.go | 59 +++++--- internal/view/table.go | 13 +- internal/watch/factory.go | 16 +- internal/watch/forwarders.go | 22 +-- 79 files changed, 2506 insertions(+), 987 deletions(-) create mode 100644 change_logs/release_v0.25.0.md delete mode 100644 internal/client/tunnel.go create mode 100644 internal/port/ann.go create mode 100644 internal/port/ann_test.go create mode 100644 internal/port/co_portspec.go create mode 100644 internal/port/co_portspec_test.go create mode 100644 internal/port/pf.go create mode 100644 internal/port/pf_test.go create mode 100644 internal/port/pfs.go create mode 100644 internal/port/pfs_test.go create mode 100644 internal/port/tunnel.go create mode 100644 internal/port/tunnel_test.go diff --git a/Makefile b/Makefile index fddde057..4525753c 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PACKAGE := github.com/derailed/$(NAME) GIT_REV ?= $(shell git rev-parse --short HEAD) SOURCE_DATE_EPOCH ?= $(shell date +%s) DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") -VERSION ?= v0.24.15 +VERSION ?= v0.25.0 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/README.md b/README.md index 2e5a7225..129dd2ff 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,8 @@ K9s uses aliases to navigate most K8s resources. ## K9s Configuration - K9s keeps its configurations inside of a `k9s` directory and the location depends on your operating system. K9s leverages [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) to load its various configurations files. + K9s keeps its configurations inside of a `k9s` directory and the location depends on your operating system. K9s leverages [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) to load its various configurations files. For information on the default locations for your OS please see [this link](https://github.com/adrg/xdg/blob/master/README.md). If you are still confused a quick `k9s info` will reveal where k9s is loading its configurations from. + | Unix | macOS | Windows | |-----------------|-----------------------------|-----------------------| diff --git a/change_logs/release_v0.25.0.md b/change_logs/release_v0.25.0.md new file mode 100644 index 00000000..96c0bcda --- /dev/null +++ b/change_logs/release_v0.25.0.md @@ -0,0 +1,121 @@ + + +# Release v0.25.0 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated! + +If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## ♫ Sounds Behind The Release ♭ + +* [High Fidelity - By Elvis Costello (Yup! he started is career as a computer operator. Can u tell??)](https://www.youtube.com/watch?v=DJS-2kacmpU) +* [Walk With A Big Stick - Foster The People](https://www.youtube.com/watch?v=XMY1VMTyl8s) +* [Beirut - Steps Ahead -- Love this band!! with the ever so talented and sadly late Michael Brecker ;(](https://www.youtube.com/watch?v=UExKTZ3veB8) + +--- + +### A Word From Our Sponsors... + +I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`! + +* [Andrew Regan](https://github.com/poblish) +* [Bruno Brito](https://github.com/brunohbrito) +* [ScubaDrew](https://github.com/ScubaDrew) +* [mike-code](https://github.com/mike-code) +* [Andrew Aadland](https://github.com/DaemonDude23) +* [Michael Albers](https://github.com/michaeljohnalbers) + +So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large. + +Also please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us! + +Thank you!! + +--- + +## Personal Note... + +I had so many distractions this cycle so expect some `disturbance in the farce!` on this drop. +To boot rat holed quiet a bit on improving speed. So I might have drop some stuff on the floor in the process... +Please report back if that's the case and we will address shortly. Tx!! + +## Port It Forward?? + +Ever been in a situation where you need to constantly port-forward on a given pod with multiple containers or exposing multiple ports? If so it might be cumbersome to have to type in the full container:port specification to activate a forward. If you fall in this use cases, you can now specify which container and port you would rather port-forward to by default. In this drop, we introduce a new annotation that you can use to specify and container/port to forward to by default. If set, the port-forward dialog will know to default to your settings. + +> NOTE: you can either use a container port name or number in your annotation! + +```yaml +# Pod fred +apiVersion: v1 +kind: Pod +metadata: + name: fred + annotations: + k9scli.io/auto-portforwards: zorg::5556 # => will default to container zorg port 5556 and local port 5566. No port-forward dialog will be shown. + # Or... + k9scli.io/portforward: bozo::6666:p1 # => launches the port-forward dialog selecting default port-forward on container bozo port named p1(8081) + # mapping to local port 6666. + ... +spec: + containers: + - name: zorg + ports: + - name: p1 + containerPort: 5556 + ... + - name: bozo + ports: + - name: p1 + containerPort: 8081 + - name: p2 + containerPort: 5555 + ... +``` + +The annotation value must specify a container to forward to as well as a local port and container port. The container port may be specified as either a port number or port name. If the local port is omitted then the local port will default to the container port number. Here are a few examples: + +1. bozo::http - creates a pf on container `bozo` with port name http. If http specifies port number 8080 then the local port will be 8080 as well. +2. bozo::9090:http - creates a pf on container `bozo` mapping local port 9090->http(8080) +3. bozo::9090:8080 - creates a pf on container `bozo` mapping local port 9090->8080 + +--- + +## Resolved Issues + +* [Issue #1299](https://github.com/derailed/k9s/issues/1299) After upgrade to 0.24.15 sorting shortcuts not working +* [Issue #1298](https://github.com/derailed/k9s/issues/1298) Install K9s through go get reporting ambiguous import error +* [Issue #1296](https://github.com/derailed/k9s/issues/1296) Crash when clicking between border of K9s and terminal pane +* [Issue #1289](https://github.com/derailed/k9s/issues/1289) Homebrew calling bottle :unneeded is deprecated! There is no replacement +* [Issue #1273](https://github.com/derailed/k9s/issues/1273) Not loading config from correct default location when XDG_CONFIG_HOME is unset +* [Issue #1268](https://github.com/derailed/k9s/issues/1268) Age sorting wrong for years +* [Issue #1258](https://github.com/derailed/k9s/issues/1258) Configurable or recent use based port-forward +* [Issue #1257](https://github.com/derailed/k9s/issues/1257) Why is the latest chocolatey on 0.24.10 +* [Issue #1243](https://github.com/derailed/k9s/issues/1243) Port forward fails in kind on windows 10 + +--- + +## PRs + +* [PR #1300](https://github.com/derailed/k9s/pull/1300) move from io/ioutil to io/os packages +* [PR #1287](https://github.com/derailed/k9s/pull/1287) Add missing styles to kiss +* [PR #1286](https://github.com/derailed/k9s/pull/1286) Some small color modifications +* [PR #1284](https://github.com/derailed/k9s/pull/1284) Fix a small typo which comes from cluster view info +* [PR #1271](https://github.com/derailed/k9s/pull/1271) Removed cursor colors that are too light to read +* [PR #1266](https://github.com/derailed/k9s/pull/1266) Skin to preserve your terminal session background color +* [PR #1264](https://github.com/derailed/k9s/pull/1205) Adding note on popeye config +* [PR #1261](https://github.com/derailed/k9s/pull/1261) Blurry logo +* [PR #1250](https://github.com/derailed/k9s/pull/1250) Gruvbox dark skin +* [PR #1249](https://github.com/derailed/k9s/pull/1249) Node shell pod tolerate all taints +* [PR #1232](https://github.com/derailed/k9s/pull/1232) Add red skin for production env +* [PR #1227](https://github.com/derailed/k9s/pull/1227) Add abbreviation ReadWriteOncePod PV access mode + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/cmd/root.go b/cmd/root.go index 4351240d..46f0daca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -94,25 +94,25 @@ func loadConfiguration() *config.Config { k9sCfg.K9s.OverrideWrite(*k9sFlags.Write) k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) - if err := k9sCfg.Refine(k8sFlags, k9sFlags); err != nil { + if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil { log.Error().Err(err).Msgf("refine failed") } conn, err := client.InitConnection(k8sCfg) k9sCfg.SetConnection(conn) if err != nil { log.Error().Err(err).Msgf("failed to connect to cluster") - } else { - // Try to access server version if that fail. Connectivity issue? - if !k9sCfg.GetConnection().CheckConnectivity() { - log.Panic().Msgf("K9s can't connect to cluster") - } - if !k9sCfg.GetConnection().ConnectionOK() { - panic("No connectivity") - } - log.Info().Msg("✅ Kubernetes connectivity") - if err := k9sCfg.Save(); err != nil { - log.Error().Err(err).Msg("Config save") - } + return k9sCfg + } + // Try to access server version if that fail. Connectivity issue? + if !k9sCfg.GetConnection().CheckConnectivity() { + log.Panic().Msgf("Cannot connect to cluster") + } + if !k9sCfg.GetConnection().ConnectionOK() { + panic("No connectivity") + } + log.Info().Msg("✅ Kubernetes connectivity") + if err := k9sCfg.Save(); err != nil { + log.Error().Err(err).Msg("Config save") } return k9sCfg diff --git a/go.mod b/go.mod index b9ae6990..e4ab6166 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.1.1 github.com/derailed/popeye v0.9.7 - github.com/derailed/tview v0.6.1 + github.com/derailed/tview v0.6.3 github.com/fatih/color v1.12.0 github.com/fsnotify/fsnotify v1.5.1 github.com/fvbommel/sortorder v1.0.2 @@ -22,10 +22,6 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.13 - // BOZO!! revamp with latest... - // github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec - // github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c - // github.com/openfaas/faas-provider v0.15.0 github.com/petergtz/pegomock v2.9.0+incompatible github.com/rakyll/hey v0.1.4 github.com/rs/zerolog v1.25.0 diff --git a/go.sum b/go.sum index d5dd4c96..96ed6a53 100644 --- a/go.sum +++ b/go.sum @@ -216,8 +216,8 @@ github.com/derailed/popeye v0.9.7 h1:EnOl8rwvAlN4KJo62+V7J713ZAWFQmKCrTBBBBBkbmQ github.com/derailed/popeye v0.9.7/go.mod h1:Ih3wTG7wBOuxdqz5tlCuCFq/vyB+Te/IpqY5HwgUTEA= github.com/derailed/tcell/v2 v2.3.1-rc.2 h1:9TmZB/IwL3MA1Jf4pC4rfMaPTcVYIN62IwE7X7A9emU= github.com/derailed/tcell/v2 v2.3.1-rc.2/go.mod h1:wegJ+SscH+jPjEQIAV/dI/grLTRm5R4IE2M479NDSL0= -github.com/derailed/tview v0.6.1 h1:dB+9bO7r6a1Yg1HE+XNJj61hioauJnGBFq2biC5bjAk= -github.com/derailed/tview v0.6.1/go.mod h1:5Wjopun0Jw3zxOFtafwc/GlrkFJix1hZz1oQetWpnwE= +github.com/derailed/tview v0.6.3 h1:4GFzcmuVjHYHKlLEpU8lSiUBVfHeYQEC0z5tlBLp4CI= +github.com/derailed/tview v0.6.3/go.mod h1:j2GwRsCb3NZe7lRjKIeplvZLkg8duyNWG6I4y+bZwEE= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -1144,7 +1144,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/client/client.go b/internal/client/client.go index 2b6f4f40..c3eb54ec 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -63,7 +63,7 @@ func InitConnection(config *Config) (*APIClient, error) { } err := a.supportsMetricsResources() if err != nil { - log.Error().Err(err).Msgf("Checking metrics-server") + log.Error().Err(err).Msgf("Fail to locate metrics-server") } if errors.Is(err, noMetricServerErr) || errors.Is(err, metricsUnsupportedErr) { return &a, nil @@ -120,11 +120,11 @@ func (a *APIClient) IsActiveNamespace(ns string) bool { // ActiveNamespace returns the current namespace. func (a *APIClient) ActiveNamespace() string { - ns, err := a.CurrentNamespaceName() - if err != nil { - return AllNamespaces + if ns, err := a.CurrentNamespaceName(); err == nil { + return ns } - return ns + + return AllNamespaces } func (a *APIClient) clearCache() { @@ -261,7 +261,7 @@ func (a *APIClient) CheckConnectivity() bool { a.reset() } } else { - log.Error().Err(err).Msgf("K9s can't connect to cluster") + log.Error().Err(err).Msgf("can't connect to cluster") a.connOK = false } @@ -301,11 +301,7 @@ func (a *APIClient) Dial() (kubernetes.Interface, error) { // RestConfig returns a rest api client. func (a *APIClient) RestConfig() (*restclient.Config, error) { - cfg, err := a.config.RESTConfig() - if err != nil { - return nil, err - } - return cfg, nil + return a.config.RESTConfig() } // CachedDiscovery returns a cached discovery client. @@ -430,7 +426,6 @@ func (a *APIClient) supportsMetricsResources() error { } apiGroups, err := dial.ServerGroups() if err != nil { - log.Warn().Err(err).Msgf("Unable to fetch APIGroups") return err } for _, grp := range apiGroups.Groups { diff --git a/internal/client/config.go b/internal/client/config.go index 230244ac..251d40db 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -15,32 +15,30 @@ import ( ) const ( - defaultQPS = 50 - defaultBurst = 50 defaultCallTimeoutDuration time.Duration = 5 * time.Second ) // Config tracks a kubernetes configuration. type Config struct { - flags *genericclioptions.ConfigFlags - clientConfig clientcmd.ClientConfig - rawConfig *clientcmdapi.Config - restConfig *restclient.Config - mutex *sync.RWMutex - OverrideNS bool + flags *genericclioptions.ConfigFlags + clientCfg clientcmd.ClientConfig + rawCfg *clientcmdapi.Config + mutex *sync.RWMutex + OverrideNS bool } // NewConfig returns a new k8s config or an error if the flags are invalid. func NewConfig(f *genericclioptions.ConfigFlags) *Config { return &Config{ flags: f, + // pathOptions: clientcmd.NewDefaultPathOptions(), mutex: &sync.RWMutex{}, } } // CallTimeout returns the call timeout if set or the default if not set. func (c *Config) CallTimeout() time.Duration { - if c.flags.Timeout == nil { + if !isSet(c.flags.Timeout) { return defaultCallTimeoutDuration } dur, err := time.ParseDuration(*c.flags.Timeout) @@ -51,28 +49,63 @@ func (c *Config) CallTimeout() time.Duration { return dur } +func (c *Config) RESTConfig() (*restclient.Config, error) { + return c.clientConfig().ClientConfig() +} + // Flags returns configuration flags. func (c *Config) Flags() *genericclioptions.ConfigFlags { return c.flags } -// SwitchContext changes the kubeconfig context to a new cluster. -func (c *Config) SwitchContext(name string) error { - if c.flags.Context != nil && *c.flags.Context == name { - return nil +func (c *Config) rawConfig() (*clientcmdapi.Config, error) { + if c.rawCfg != nil { + return c.rawCfg, nil } - if _, err := c.GetContext(name); err != nil { + cfg, err := c.clientConfig().RawConfig() + if err != nil { + return nil, err + } + c.rawCfg = &cfg + + return c.rawCfg, nil +} + +func (c *Config) clientConfig() clientcmd.ClientConfig { + if c.clientCfg != nil { + return c.clientCfg + } + c.clientCfg = c.flags.ToRawKubeConfigLoader() + + return c.clientCfg +} + +func (c *Config) reset() { + c.clientCfg = nil +} + +// SwitchContext changes the kubeconfig context to a new cluster. +func (c *Config) SwitchContext(name string) error { + cfg, err := c.rawConfig() + if err != nil { + return err + } + if cfg.CurrentContext == name { + return nil + } + context, err := c.GetContext(name) + if err != nil { return fmt.Errorf("context %s does not exist", name) } - c.reset() c.flags.Context = &name + c.flags.ClusterName = &(context.Cluster) return nil } -func (c *Config) reset() { - c.clientConfig, c.rawConfig, c.restConfig = nil, nil, nil +func (c *Config) RawConfig() *clientcmdapi.Config { + return c.rawCfg } // CurrentContextName returns the currently active config context. @@ -80,17 +113,17 @@ func (c *Config) CurrentContextName() (string, error) { if isSet(c.flags.Context) { return *c.flags.Context, nil } - - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return "", err } + return cfg.CurrentContext, nil } // GetContext fetch a given context or error if it does not exists. func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) { - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return nil, err } @@ -103,7 +136,7 @@ func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) { // Contexts fetch all available contexts. func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) { - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return nil, err } @@ -113,18 +146,23 @@ func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) { // DelContext remove a given context from the configuration. func (c *Config) DelContext(n string) error { - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return err } delete(cfg.Contexts, n) - return clientcmd.ModifyConfig(c.clientConfig.ConfigAccess(), cfg, true) + acc, err := c.ConfigAccess() + if err != nil { + return err + } + + return clientcmd.ModifyConfig(acc, *cfg, true) } // ContextNames fetch all available contexts. func (c *Config) ContextNames() ([]string, error) { - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return nil, err } @@ -137,16 +175,16 @@ func (c *Config) ContextNames() ([]string, error) { } // ClusterNameFromContext returns the cluster associated with the given context. -func (c *Config) ClusterNameFromContext(ctx string) (string, error) { - cfg, err := c.RawConfig() +func (c *Config) ClusterNameFromContext(context string) (string, error) { + cfg, err := c.rawConfig() if err != nil { return "", err } - if ctx, ok := cfg.Contexts[ctx]; ok { + if ctx, ok := cfg.Contexts[context]; ok { return ctx.Cluster, nil } - return "", fmt.Errorf("unable to locate cluster from context %s", ctx) + return "", fmt.Errorf("unable to locate cluster from context %s", context) } // CurrentClusterName returns the active cluster name. @@ -154,16 +192,11 @@ func (c *Config) CurrentClusterName() (string, error) { if isSet(c.flags.ClusterName) { return *c.flags.ClusterName, nil } - - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return "", err } - current := cfg.CurrentContext - if isSet(c.flags.Context) { - current = *c.flags.Context - } if ctx, ok := cfg.Contexts[current]; ok { return ctx.Cluster, nil @@ -174,7 +207,7 @@ func (c *Config) CurrentClusterName() (string, error) { // ClusterNames fetch all kubeconfig defined clusters. func (c *Config) ClusterNames() ([]string, error) { - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return nil, err } @@ -224,7 +257,7 @@ func (c *Config) CurrentUserName() (string, error) { return *c.flags.AuthInfoName, nil } - cfg, err := c.RawConfig() + cfg, err := c.rawConfig() if err != nil { return "", err } @@ -242,25 +275,9 @@ func (c *Config) CurrentUserName() (string, error) { // CurrentNamespaceName retrieves the active namespace. func (c *Config) CurrentNamespaceName() (string, error) { - if isSet(c.flags.Namespace) { - return *c.flags.Namespace, nil - } + ns, _, err := c.clientConfig().Namespace() - cfg, err := c.RawConfig() - if err != nil { - return "", err - } - ctx, err := c.CurrentContextName() - if err != nil { - return "", err - } - if ctx, ok := cfg.Contexts[ctx]; ok { - if isSet(&ctx.Namespace) { - return ctx.Namespace, nil - } - } - - return "", fmt.Errorf("No active namespace specified") + return ns, err } // NamespaceNames fetch all available namespaces on current cluster. @@ -278,51 +295,7 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { c.mutex.RLock() defer c.mutex.RUnlock() - c.ensureConfig() - return c.clientConfig.ConfigAccess(), nil -} - -// RawConfig fetch the current kubeconfig with no overrides. -func (c *Config) RawConfig() (clientcmdapi.Config, error) { - c.mutex.Lock() - defer c.mutex.Unlock() - - if c.rawConfig == nil { - c.ensureConfig() - cfg, err := c.clientConfig.RawConfig() - if err != nil { - return cfg, err - } - c.rawConfig = &cfg - if c.flags.Context == nil { - c.flags.Context = &c.rawConfig.CurrentContext - } - } - - return *c.rawConfig, nil -} - -// RESTConfig fetch the current REST api service connection. -func (c *Config) RESTConfig() (*restclient.Config, error) { - if c.restConfig != nil { - return c.restConfig, nil - } - - var err error - if c.restConfig, err = c.flags.ToRESTConfig(); err != nil { - return nil, err - } - c.restConfig.QPS = defaultQPS - c.restConfig.Burst = defaultBurst - - return c.restConfig, nil -} - -func (c *Config) ensureConfig() { - if c.clientConfig != nil { - return - } - c.clientConfig = c.flags.ToRawKubeConfigLoader() + return c.clientConfig().ConfigAccess(), nil } // ---------------------------------------------------------------------------- diff --git a/internal/client/config_test.go b/internal/client/config_test.go index 46137574..bb00208c 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -2,7 +2,6 @@ package client_test import ( "errors" - "fmt" "testing" "github.com/derailed/k9s/internal/client" @@ -18,98 +17,151 @@ func init() { } func TestConfigCurrentContext(t *testing.T) { - name, kubeConfig := "blee", "./testdata/config" - uu := []struct { - flags *genericclioptions.ConfigFlags + var kubeConfig = "./testdata/config" + + uu := map[string]struct { context string + e string }{ - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"}, - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &name}, "blee"}, + "default": { + e: "fred", + }, + "custom": { + context: "blee", + e: "blee", + }, } - for _, u := range uu { - cfg := client.NewConfig(u.flags) - ctx, err := cfg.CurrentContextName() - assert.Nil(t, err) - assert.Equal(t, u.context, ctx) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + flags := genericclioptions.NewConfigFlags(false) + flags.KubeConfig = &kubeConfig + if u.context != "" { + flags.Context = &u.context + } + cfg := client.NewConfig(flags) + ctx, err := cfg.CurrentContextName() + assert.Nil(t, err) + assert.Equal(t, u.e, ctx) + }) } } func TestConfigCurrentCluster(t *testing.T) { name, kubeConfig := "blee", "./testdata/config" - uu := []struct { + uu := map[string]struct { flags *genericclioptions.ConfigFlags cluster string }{ - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"}, - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name}, "blee"}, + "default": { + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, + cluster: "fred", + }, + "custom": { + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name}, + cluster: "blee", + }, } - for _, u := range uu { - cfg := client.NewConfig(u.flags) - ctx, err := cfg.CurrentClusterName() - assert.Nil(t, err) - assert.Equal(t, u.cluster, ctx) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := client.NewConfig(u.flags) + ctx, err := cfg.CurrentClusterName() + assert.Nil(t, err) + assert.Equal(t, u.cluster, ctx) + }) } } func TestConfigCurrentUser(t *testing.T) { name, kubeConfig := "blee", "./testdata/config" - uu := []struct { + uu := map[string]struct { flags *genericclioptions.ConfigFlags user string }{ - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"}, - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, AuthInfoName: &name}, "blee"}, + "default": { + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, + user: "fred", + }, + "custom": { + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, AuthInfoName: &name}, + user: "blee", + }, } - for _, u := range uu { - cfg := client.NewConfig(u.flags) - ctx, err := cfg.CurrentUserName() - assert.Nil(t, err) - assert.Equal(t, u.user, ctx) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := client.NewConfig(u.flags) + ctx, err := cfg.CurrentUserName() + assert.Nil(t, err) + assert.Equal(t, u.user, ctx) + }) } } func TestConfigCurrentNamespace(t *testing.T) { - name, kubeConfig := "blee", "./testdata/config" - uu := []struct { + kubeConfig := "./testdata/config" + bleeNS, bleeCTX := "blee", "blee" + uu := map[string]struct { flags *genericclioptions.ConfigFlags namespace string - err error }{ - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "", fmt.Errorf("No active namespace specified")}, - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &name}, "blee", nil}, + "default": { + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, + namespace: "default", + }, + "withContext": { + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &bleeCTX}, + namespace: "zorg", + }, + "withNS": { + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &bleeNS}, + namespace: "blee", + }, } - for _, u := range uu { - cfg := client.NewConfig(u.flags) - ns, err := cfg.CurrentNamespaceName() - assert.Equal(t, u.err, err) - assert.Equal(t, u.namespace, ns) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := client.NewConfig(u.flags) + ns, err := cfg.CurrentNamespaceName() + assert.Nil(t, err) + assert.Equal(t, u.namespace, ns) + }) } } func TestConfigGetContext(t *testing.T) { kubeConfig := "./testdata/config" - uu := []struct { - flags *genericclioptions.ConfigFlags + uu := map[string]struct { cluster string err error }{ - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "blee", nil}, - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "bozo", errors.New("invalid context `bozo specified")}, + "default": { + cluster: "blee", + }, + "custom": { + cluster: "bozo", + err: errors.New("invalid context `bozo specified"), + }, } - for _, u := range uu { - cfg := client.NewConfig(u.flags) - ctx, err := cfg.GetContext(u.cluster) - if err != nil { - assert.Equal(t, u.err, err) - } else { - assert.NotNil(t, ctx) - assert.Equal(t, u.cluster, ctx.Cluster) - } + flags := &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig} + cfg := client.NewConfig(flags) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + ctx, err := cfg.GetContext(u.cluster) + if err != nil { + assert.Equal(t, u.err, err) + } else { + assert.NotNil(t, ctx) + assert.Equal(t, u.cluster, ctx.Cluster) + } + }) } } @@ -205,7 +257,8 @@ func TestConfigDelContext(t *testing.T) { assert.Nil(t, err) cc, err := cfg.ContextNames() assert.Nil(t, err) - assert.Equal(t, 2, len(cc)) + assert.Equal(t, 1, len(cc)) + assert.Equal(t, "blee", cc[0]) } func TestConfigRestConfig(t *testing.T) { diff --git a/internal/client/testdata/config b/internal/client/testdata/config index 5541a687..88e0a0e8 100644 --- a/internal/client/testdata/config +++ b/internal/client/testdata/config @@ -2,42 +2,43 @@ apiVersion: v1 kind: Config preferences: {} clusters: -- cluster: - insecure-skip-tls-verify: true - server: https://localhost:3000 - name: fred -- cluster: - insecure-skip-tls-verify: true - server: https://localhost:3001 - name: blee -- cluster: - insecure-skip-tls-verify: true - server: https://localhost:3002 - name: duh + - cluster: + insecure-skip-tls-verify: true + server: https://localhost:3000 + name: fred + - cluster: + insecure-skip-tls-verify: true + server: https://localhost:3001 + name: blee + - cluster: + insecure-skip-tls-verify: true + server: https://localhost:3002 + name: duh contexts: -- context: - cluster: fred - user: fred - name: fred -- context: - cluster: blee - user: blee - name: blee -- context: - cluster: duh - user: duh - name: duh + - context: + cluster: fred + user: fred + name: fred + - context: + cluster: blee + user: blee + namespace: zorg + name: blee + - context: + cluster: duh + user: duh + name: duh current-context: fred users: -- name: fred - user: - client-certificate-data: ZnJlZA== - client-key-data: ZnJlZA== -- name: blee - user: - client-certificate-data: ZnJlZA== - client-key-data: ZnJlZA== -- name: duh - user: - client-certificate-data: ZnJlZA== - client-key-data: ZnJlZA== + - name: fred + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== + - name: blee + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== + - name: duh + user: + client-certificate-data: ZnJlZA== + client-key-data: ZnJlZA== diff --git a/internal/client/testdata/config.1 b/internal/client/testdata/config.1 index 9c2ff1e3..640d0ef2 100644 --- a/internal/client/testdata/config.1 +++ b/internal/client/testdata/config.1 @@ -7,33 +7,13 @@ clusters: - cluster: insecure-skip-tls-verify: true server: https://localhost:3002 - name: duh -- cluster: - insecure-skip-tls-verify: true - server: https://localhost:3000 name: fred contexts: - context: cluster: blee user: blee name: blee -- context: - cluster: duh - user: duh - name: duh -current-context: fred +current-context: blee kind: Config preferences: {} -users: -- name: blee - user: - client-certificate-data: ZnJlZA== - client-key-data: ZnJlZA== -- name: duh - user: - client-certificate-data: ZnJlZA== - client-key-data: ZnJlZA== -- name: fred - user: - client-certificate-data: ZnJlZA== - client-key-data: ZnJlZA== +users: null diff --git a/internal/client/tunnel.go b/internal/client/tunnel.go deleted file mode 100644 index 2eba5a37..00000000 --- a/internal/client/tunnel.go +++ /dev/null @@ -1,11 +0,0 @@ -package client - -// PortTunnel represents a host tunnel port mapper. -type PortTunnel struct { - Address, LocalPort, ContainerPort string -} - -// PortMap returns a port mapping. -func (t PortTunnel) PortMap() string { - return t.LocalPort + ":" + t.ContainerPort -} diff --git a/internal/config/config.go b/internal/config/config.go index 90f7c73f..10a52708 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path" "path/filepath" "github.com/adrg/xdg" @@ -59,6 +60,15 @@ func K9sHome() string { if env := os.Getenv(K9sConfig); env != "" { return env } + if env := os.Getenv("XDG_CONFIG_HOME"); env == "" { + dir, err := os.UserHomeDir() + if err != nil { + log.Error().Err(err).Msgf("user home dir") + return "" + } + return path.Join(dir, ".config", "k9s") + } + xdgK9sHome, err := xdg.ConfigFile("k9s") if err != nil { log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s") @@ -73,21 +83,25 @@ func NewConfig(ks KubeSettings) *Config { } // Refine the configuration based on cli args. -func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags) error { - cfg, err := flags.ToRawKubeConfigLoader().RawConfig() - if err != nil { - return err - } +func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error { if isSet(flags.Context) { c.K9s.CurrentContext = *flags.Context } else { - c.K9s.CurrentContext = cfg.CurrentContext + context, err := cfg.CurrentContextName() + if err != nil { + return err + } + c.K9s.CurrentContext = context } log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext) if c.K9s.CurrentContext == "" { return errors.New("Invalid kubeconfig context detected") } - context, ok := cfg.Contexts[c.K9s.CurrentContext] + cc, err := cfg.Contexts() + if err != nil { + return err + } + context, ok := cc[c.K9s.CurrentContext] if !ok { return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 55f37e4d..f840f6c0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" m "github.com/petergtz/pegomock" "github.com/rs/zerolog" @@ -63,7 +64,7 @@ func TestConfigRefine(t *testing.T) { m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) cfg := config.NewConfig(mk) - err := cfg.Refine(u.flags, nil) + err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags)) if u.issue { assert.NotNil(t, err) } else { @@ -87,7 +88,6 @@ func TestConfigValidate(t *testing.T) { cfg.SetConnection(mc) assert.Nil(t, cfg.Load("testdata/k9s.yml")) cfg.Validate() - // mc.VerifyWasCalledOnce().ValidNamespaces() } func TestConfigLoad(t *testing.T) { diff --git a/internal/config/plugin.go b/internal/config/plugin.go index 8cb8f48a..2e1fbe39 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -1,8 +1,10 @@ package config import ( + "fmt" "os" "path/filepath" + "strings" "gopkg.in/yaml.v2" ) @@ -20,12 +22,17 @@ type Plugin struct { Scopes []string `yaml:"scopes"` Args []string `yaml:"args"` ShortCut string `yaml:"shortCut"` + Pipes []string `yaml:"pipes"` Description string `yaml:"description"` Command string `yaml:"command"` Confirm bool `yaml:"confirm"` Background bool `yaml:"background"` } +func (p Plugin) String() string { + return fmt.Sprintf("[%s] %s(%s)", p.ShortCut, p.Command, strings.Join(p.Args, " ")) +} + // NewPlugins returns a new plugin. func NewPlugins() Plugins { return Plugins{ diff --git a/internal/dao/context.go b/internal/dao/context.go index 50cad101..47ec9bb8 100644 --- a/internal/dao/context.go +++ b/internal/dao/context.go @@ -2,6 +2,7 @@ package dao import ( "context" + "errors" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" @@ -63,14 +64,14 @@ func (c *Context) Switch(ctx string) error { // KubeUpdate modifies kubeconfig default context. func (c *Context) KubeUpdate(n string) error { - config, err := c.config().RawConfig() - if err != nil { - return err + cfg := c.config().RawConfig() + if cfg == nil { + return errors.New("unable to fetch raw config") } if err := c.Switch(n); err != nil { return err } return clientcmd.ModifyConfig( - clientcmd.NewDefaultPathOptions(), config, true, + clientcmd.NewDefaultPathOptions(), *cfg, true, ) } diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 8138ce55..90dd4ba0 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -69,20 +69,20 @@ func (d *Deployment) Restart(ctx context.Context, path string) error { return err } - ns, _ := client.Namespaced(path) - auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb}) + auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", []string{client.PatchVerb}) if err != nil { return err } if !auth { return fmt.Errorf("user is not authorized to restart a deployment") } - update, err := polymorphichelpers.ObjectRestarterFn(dp) + + dial, err := d.Client().Dial() if err != nil { return err } - dial, err := d.Client().Dial() + restarter, err := polymorphichelpers.ObjectRestarterFn(dp) if err != nil { return err } @@ -91,7 +91,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error { ctx, dp.Name, types.StrategicMergePatchType, - update, + restarter, metav1.PatchOptions{}, ) return err diff --git a/internal/dao/ds.go b/internal/dao/ds.go index d46d4f88..761facea 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -86,7 +86,7 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) e return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) } -func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts *LogOptions) error { +func podLogs(ctx context.Context, out LogChan, sel map[string]string, opts *LogOptions) error { f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { return errors.New("expecting a context factory") @@ -110,14 +110,13 @@ func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts *LogOpt po := Pod{} po.Init(f, client.NewGVR("v1/pods")) for _, o := range oo { - var pod v1.Pod - err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) - if err != nil { - return err + u, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expected unstructured got %t", o) } opts = opts.Clone() - opts.Path = client.FQN(pod.Namespace, pod.Name) - if err := po.TailLogs(ctx, c, opts); err != nil { + opts.Path = client.FQN(u.GetNamespace(), u.GetName()) + if err := po.TailLogs(ctx, out, opts); err != nil { return err } } diff --git a/internal/dao/helm.go b/internal/dao/helm.go index 78f10260..0845e7b8 100644 --- a/internal/dao/helm.go +++ b/internal/dao/helm.go @@ -111,8 +111,7 @@ func (c *Helm) Delete(path string, cascade, force bool) error { // EnsureHelmConfig return a new configuration. func (c *Helm) EnsureHelmConfig(ns string) (*action.Configuration, error) { cfg := new(action.Configuration) - flags := c.Client().Config().Flags() - if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil { + if err := cfg.Init(c.Client().Config().Flags(), ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil { return nil, err } return cfg, nil diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go index 7242b265..0193f2f3 100644 --- a/internal/dao/log_item.go +++ b/internal/dao/log_item.go @@ -2,39 +2,31 @@ package dao import ( "bytes" - "fmt" - "regexp" - "time" - - "github.com/derailed/k9s/internal/color" ) // LogChan represents a channel for logs. type LogChan chan *LogItem +var ItemEOF = new(LogItem) + // LogItem represents a container log line. type LogItem struct { - Pod, Container, Timestamp string - SingleContainer bool - Bytes []byte + Pod, Container string + SingleContainer bool + Bytes []byte } // NewLogItem returns a new item. -func NewLogItem(b []byte) *LogItem { - space := []byte(" ") - cols := bytes.Split(b[:len(b)-1], space) - +func NewLogItem(bb []byte) *LogItem { return &LogItem{ - Timestamp: string(cols[0]), - Bytes: bytes.Join(cols[1:], space), + Bytes: bb, } } // NewLogItemFromString returns a new item. func NewLogItemFromString(s string) *LogItem { return &LogItem{ - Bytes: []byte(s), - Timestamp: time.Now().String(), + Bytes: []byte(s), } } @@ -46,22 +38,18 @@ func (l *LogItem) ID() string { return l.Container } -// Clone copies an item. -func (l *LogItem) Clone() *LogItem { - bytes := make([]byte, len(l.Bytes)) - copy(bytes, l.Bytes) - return &LogItem{ - Container: l.Container, - Pod: l.Pod, - Timestamp: l.Timestamp, - SingleContainer: l.SingleContainer, - Bytes: bytes, +// GetTimestamp fetch log lime timestamp +func (l *LogItem) GetTimestamp() string { + index := bytes.Index(l.Bytes, []byte{' '}) + if index < 0 { + return "" } + return string(l.Bytes[:index]) } // Info returns pod and container information. func (l *LogItem) Info() string { - return fmt.Sprintf("%q::%q", l.Pod, l.Container) + return l.Pod + "::" + l.Container } // IsEmpty checks if the entry is empty. @@ -69,37 +57,39 @@ func (l *LogItem) IsEmpty() bool { return len(l.Bytes) == 0 } -var ( - escPattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) - matcher = []byte("$1[]") -) +// Size returns the size of the item. +func (l *LogItem) Size() int { + return 100 + len(l.Bytes) + len(l.Pod) + len(l.Container) +} // Render returns a log line as string. -func (l *LogItem) Render(paint int, showTime bool) []byte { - bb := make([]byte, 0, 200) - if showTime { - t := l.Timestamp - for i := len(t); i < 30; i++ { - t += " " +func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) { + index := bytes.Index(l.Bytes, []byte{' '}) + if showTime && index > 0 { + bb.WriteString("[gray::]") + bb.Write(l.Bytes[:index]) + bb.WriteString(" ") + for i := len(l.Bytes[:index]); i < 30; i++ { + bb.WriteByte(' ') } - bb = append(bb, color.ANSIColorize(t, 106)...) - bb = append(bb, ' ') } - var hasPod bool if l.Pod != "" { - bb = append(bb, color.ANSIColorize(l.Pod, paint)...) - hasPod = true - } - if !l.SingleContainer && l.Container != "" { - if hasPod { - bb = append(bb, ':') - } - bb = append(bb, color.ANSIColorize(l.Container, paint)...) - bb = append(bb, ' ') - } else if hasPod { - bb = append(bb, ' ') + bb.WriteString("[" + paint + "::]" + l.Pod) } - return append(bb, escPattern.ReplaceAll(l.Bytes, matcher)...) + if !l.SingleContainer && l.Container != "" { + if len(l.Pod) > 0 { + bb.WriteString(" ") + } + bb.WriteString("[" + paint + "::b]" + l.Container + "[-::-] ") + } else if len(l.Pod) > 0 { + bb.WriteString("[-::]") + } + + if index > 0 { + bb.Write(l.Bytes[index+1:]) + } else { + bb.Write(l.Bytes) + } } diff --git a/internal/dao/log_item_test.go b/internal/dao/log_item_test.go index 73452061..63116cd6 100644 --- a/internal/dao/log_item_test.go +++ b/internal/dao/log_item_test.go @@ -1,6 +1,7 @@ package dao_test import ( + "bytes" "fmt" "testing" @@ -34,13 +35,13 @@ func TestLogItemRender(t *testing.T) { }{ "empty": { opts: dao.LogOptions{}, - e: "Testing 1,2,3...", + e: "Testing 1,2,3...\n", }, "container": { opts: dao.LogOptions{ Container: "fred", }, - e: "\x1b[38;5;0mfred\x1b[0m Testing 1,2,3...", + e: "[yellow::b]fred[-::-] Testing 1,2,3...\n", }, "pod": { opts: dao.LogOptions{ @@ -48,7 +49,7 @@ func TestLogItemRender(t *testing.T) { Container: "blee", SingleContainer: true, }, - e: "\x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...", + e: "[yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n", }, "full": { opts: dao.LogOptions{ @@ -57,7 +58,7 @@ func TestLogItemRender(t *testing.T) { SingleContainer: true, ShowTimestamp: true, }, - e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00\x1b[0m \x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...", + e: "[gray::]2018-12-14T10:36:43.326972-07:00 [yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n", }, } @@ -69,7 +70,9 @@ func TestLogItemRender(t *testing.T) { _, n := client.Namespaced(u.opts.Path) i.Pod, i.Container = n, u.opts.Container - assert.Equal(t, u.e, string(i.Render(0, u.opts.ShowTimestamp))) + bb := bytes.NewBuffer(make([]byte, 0, i.Size())) + i.Render("yellow", u.opts.ShowTimestamp, bb) + assert.Equal(t, u.e, bb.String()) }) } } @@ -78,9 +81,24 @@ func BenchmarkLogItemRender(b *testing.B) { s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3...")) i := dao.NewLogItem(s) i.Pod, i.Container = "fred", "blee" + b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { - i.Render(0, true) + bb := bytes.NewBuffer(make([]byte, 0, i.Size())) + i.Render("yellow", true, bb) + } +} + +func BenchmarkLogItemRenderNoTS(b *testing.B) { + s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3...")) + i := dao.NewLogItem(s) + i.Pod, i.Container = "fred", "blee" + + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + bb := bytes.NewBuffer(make([]byte, 0, i.Size())) + i.Render("yellow", false, bb) } } diff --git a/internal/dao/log_items.go b/internal/dao/log_items.go index d50b401a..888c3db3 100644 --- a/internal/dao/log_items.go +++ b/internal/dao/log_items.go @@ -1,37 +1,37 @@ package dao import ( + "bytes" "fmt" "regexp" "strings" "sync" - "github.com/gdamore/tcell/v2" "github.com/sahilm/fuzzy" ) -var colorPalette = []tcell.Color{ - tcell.ColorTeal, - tcell.ColorGreen, - tcell.ColorPurple, - tcell.ColorLime, - tcell.ColorBlue, - tcell.ColorYellow, - tcell.ColorFuchsia, - tcell.ColorAqua, +var podPalette = []string{ + "teal", + "green", + "purple", + "lime", + "blue", + "yellow", + "fushia", + "aqua", } // LogItems represents a collection of log items. type LogItems struct { - items []*LogItem - colors map[string]tcell.Color - mx sync.RWMutex + items []*LogItem + podColors map[string]string + mx sync.RWMutex } // NewLogItems returns a new instance. func NewLogItems() *LogItems { return &LogItems{ - colors: make(map[string]tcell.Color), + podColors: make(map[string]string), } } @@ -56,9 +56,9 @@ func (l *LogItems) Clear() { l.mx.Lock() defer l.mx.Unlock() - l.items = nil - for k := range l.colors { - delete(l.colors, k) + l.items = l.items[:0] + for k := range l.podColors { + delete(l.podColors, k) } } @@ -76,8 +76,8 @@ func (l *LogItems) Subset(index int) *LogItems { defer l.mx.RUnlock() return &LogItems{ - items: l.items[index:], - colors: l.colors, + items: l.items[index:], + podColors: l.podColors, } } @@ -87,8 +87,8 @@ func (l *LogItems) Merge(n *LogItems) { defer l.mx.Unlock() l.items = append(l.items, n.items...) - for k, v := range n.colors { - l.colors[k] = v + for k, v := range n.podColors { + l.podColors[k] = v } } @@ -101,47 +101,60 @@ func (l *LogItems) Add(ii ...*LogItem) { } // Lines returns a collection of log lines. -func (l *LogItems) Lines(showTime bool) [][]byte { +func (l *LogItems) Lines(index int, showTime bool, ll [][]byte) { l.mx.Lock() defer l.mx.Unlock() - ll := make([][]byte, len(l.items)) - for i, item := range l.items { - color := l.colors[item.ID()] - ll[i] = item.Render(int(color-tcell.ColorValid), showTime) + var colorIndex int + for i, item := range l.items[index:] { + id := item.ID() + color, ok := l.podColors[id] + if !ok { + if colorIndex >= len(podPalette) { + colorIndex = 0 + } + color = podPalette[colorIndex] + l.podColors[id] = color + colorIndex++ + } + bb := bytes.NewBuffer(make([]byte, 0, item.Size())) + item.Render(color, showTime, bb) + ll[i] = bb.Bytes() } - - return ll } // StrLines returns a collection of log lines. -func (l *LogItems) StrLines(showTime bool) []string { +func (l *LogItems) StrLines(index int, showTime bool) []string { l.mx.Lock() defer l.mx.Unlock() - ll := make([]string, len(l.items)) - for i, item := range l.items { - ll[i] = string(item.Render(0, showTime)) + ll := make([]string, len(l.items[index:])) + for i, item := range l.items[index:] { + bb := bytes.NewBuffer(make([]byte, 0, item.Size())) + item.Render("white", showTime, bb) + ll[i] = bb.String() } return ll } // Render returns logs as a collection of strings. -func (l *LogItems) Render(showTime bool, ll [][]byte) { - index := len(l.colors) - for i, item := range l.items { +func (l *LogItems) Render(index int, showTime bool, ll [][]byte) { + var colorIndex int + for i, item := range l.items[index:] { id := item.ID() - color, ok := l.colors[id] + color, ok := l.podColors[id] if !ok { - if index >= len(colorPalette) { - index = 0 + if colorIndex >= len(podPalette) { + colorIndex = 0 } - color = colorPalette[index] - l.colors[id] = color - index++ + color = podPalette[colorIndex] + l.podColors[id] = color + colorIndex++ } - ll[i] = item.Render(int(color-tcell.ColorValid), showTime) + bb := bytes.NewBuffer(make([]byte, 0, item.Size())) + item.Render(color, showTime, bb) + ll[i] = bb.Bytes() } } @@ -154,15 +167,15 @@ func (l *LogItems) DumpDebug(m string) { } // Filter filters out log items based on given filter. -func (l *LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) { +func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, error) { if q == "" { return nil, nil, nil } if IsFuzzySelector(q) { - mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:]), showTime) + mm, ii := l.fuzzyFilter(index, strings.TrimSpace(q[2:]), showTime) return mm, ii, nil } - matches, indices, err := l.filterLogs(q, showTime) + matches, indices, err := l.filterLogs(index, q, showTime) if err != nil { return nil, nil, err } @@ -170,10 +183,10 @@ func (l *LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) { return matches, indices, nil } -func (l *LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) { +func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]int) { q = strings.TrimSpace(q) matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10) - mm := fuzzy.Find(q, l.StrLines(showTime)) + mm := fuzzy.Find(q, l.StrLines(index, showTime)) for _, m := range mm { matches = append(matches, m.Index) indices = append(indices, m.MatchedIndexes) @@ -182,7 +195,7 @@ func (l *LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) { return matches, indices } -func (l *LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) { +func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]int, error) { var invert bool if IsInverseSelector(q) { invert = true @@ -193,7 +206,9 @@ func (l *LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) { return nil, nil, err } matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10) - for i, line := range l.Lines(showTime) { + ll := make([][]byte, len(l.items[index:])) + l.Lines(index, showTime, ll) + for i, line := range ll { locs := rx.FindIndex(line) if locs != nil && invert { continue diff --git a/internal/dao/log_items_test.go b/internal/dao/log_items_test.go index b783081f..6914b424 100644 --- a/internal/dao/log_items_test.go +++ b/internal/dao/log_items_test.go @@ -71,7 +71,7 @@ func TestLogItemsFilter(t *testing.T) { for _, i := range ii.Items() { i.Pod, i.Container = n, u.opts.Container } - res, _, err := ii.Filter(u.q, false) + res, _, err := ii.Filter(0, u.q, false) assert.Equal(t, u.err, err) if err == nil { assert.Equal(t, u.e, res) @@ -87,20 +87,20 @@ func TestLogItemsRender(t *testing.T) { }{ "empty": { opts: dao.LogOptions{}, - e: "Testing 1,2,3...", + e: "Testing 1,2,3...\n", }, "container": { opts: dao.LogOptions{ Container: "fred", }, - e: "\x1b[38;5;6mfred\x1b[0m Testing 1,2,3...", + e: "[teal::b]fred[-::-] Testing 1,2,3...\n", }, - "pod": { + "pod-container": { opts: dao.LogOptions{ Path: "blee/fred", Container: "blee", }, - e: "\x1b[38;5;6mfred\x1b[0m:\x1b[38;5;6mblee\x1b[0m Testing 1,2,3...", + e: "[teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n", }, "full": { opts: dao.LogOptions{ @@ -108,7 +108,7 @@ func TestLogItemsRender(t *testing.T) { Container: "blee", ShowTimestamp: true, }, - e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00\x1b[0m \x1b[38;5;6mfred\x1b[0m:\x1b[38;5;6mblee\x1b[0m Testing 1,2,3...", + e: "[gray::]2018-12-14T10:36:43.326972-07:00 [teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n", }, } @@ -121,7 +121,7 @@ func TestLogItemsRender(t *testing.T) { ii.Items()[0].Pod, ii.Items()[0].Container = n, u.opts.Container t.Run(k, func(t *testing.T) { res := make([][]byte, 1) - ii.Render(u.opts.ShowTimestamp, res) + ii.Render(0, u.opts.ShowTimestamp, res) assert.Equal(t, u.e, string(res[0])) }) } diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index 208e13e6..dba05e72 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -2,7 +2,6 @@ package dao import ( "fmt" - "strings" "time" "github.com/derailed/k9s/internal/client" @@ -12,12 +11,14 @@ import ( // LogOptions represents logger options. type LogOptions struct { + CreateDuration time.Duration Path string Container string DefaultContainer string SinceTime string Lines int64 SinceSeconds int64 + Head bool Previous bool SingleContainer bool MultiPods bool @@ -77,6 +78,18 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions { Previous: o.Previous, TailLines: &o.Lines, } + if o.Head { + var maxBytes int64 = 1000 + //var defaultTail int64 = -1 + //var defaultSince int64 + + opts.Follow = false + opts.TailLines, opts.SinceSeconds, opts.SinceTime = nil, nil, nil + //opts.TailLines = &defaultTail + //opts.SinceSeconds = &defaultSince + opts.LimitBytes = &maxBytes + return &opts + } if o.SinceSeconds < 0 { return &opts } @@ -96,21 +109,6 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions { return &opts } -// FixedSizeName returns a normalize fixed size pod name if possible. -func (o *LogOptions) FixedSizeName() string { - _, n := client.Namespaced(o.Path) - tokens := strings.Split(n, "-") - if len(tokens) < 3 { - return n - } - var s []string - for i := 0; i < len(tokens)-1; i++ { - s = append(s, tokens[i]) - } - - return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1] -} - // DecorateLog add a log header to display po/co information along with the log message. func (o *LogOptions) DecorateLog(bytes []byte) *LogItem { item := NewLogItem(bytes) diff --git a/internal/dao/pod.go b/internal/dao/pod.go index d4184ba9..4a33d37a 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -177,7 +177,7 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) { } // TailLogs tails a given container logs. -func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error { +func (p *Pod) TailLogs(ctx context.Context, out LogChan, opts *LogOptions) error { log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container) fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { @@ -198,18 +198,18 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error { if co, ok := GetDefaultLogContainer(po.ObjectMeta, po.Spec); ok && !opts.AllContainers { opts.DefaultContainer = co - return tailLogs(ctx, p, c, opts) + return tailLogs(ctx, p, out, opts) } if opts.HasContainer() && !opts.AllContainers { - return tailLogs(ctx, p, c, opts) + return tailLogs(ctx, p, out, opts) } var tailed bool for _, co := range po.Spec.InitContainers { o := opts.Clone() o.Container = co.Name - if err := tailLogs(ctx, p, c, o); err != nil { + if err := tailLogs(ctx, p, out, o); err != nil { return err } tailed = true @@ -217,7 +217,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error { for _, co := range po.Spec.Containers { o := opts.Clone() o.Container = co.Name - if err := tailLogs(ctx, p, c, o); err != nil { + if err := tailLogs(ctx, p, out, o); err != nil { return err } tailed = true @@ -225,7 +225,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error { for _, co := range po.Spec.EphemeralContainers { o := opts.Clone() o.Container = co.Name - if err := tailLogs(ctx, p, c, o); err != nil { + if err := tailLogs(ctx, p, out, o); err != nil { return err } tailed = true @@ -326,21 +326,22 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error // ---------------------------------------------------------------------------- // Helpers... -func tailLogs(ctx context.Context, logger Logger, c LogChan, opts *LogOptions) error { - log.Debug().Msgf("Tailing logs for %#v", opts) - +func tailLogs(ctx context.Context, logger Logger, out LogChan, opts *LogOptions) error { var ( err error req *restclient.Request stream io.ReadCloser ) + + o := opts.ToPodLogOptions() + log.Debug().Msgf("TAIL_LOGS! %#v", o) done: for r := 0; r < logRetryCount; r++ { - req, err = logger.Logs(opts.Path, opts.ToPodLogOptions()) + req, err = logger.Logs(opts.Path, o) if err == nil { // This call will block if nothing is in the stream!! if stream, err = req.Stream(ctx); err == nil { - go readLogs(stream, c, opts) + go readLogs(ctx, stream, out, opts) break } else { log.Error().Err(err).Msg("Streaming logs") @@ -351,6 +352,7 @@ done: select { case <-ctx.Done(): + log.Debug().Msgf("!!!!TAIL_LOGS CANCELED!!!!") err = ctx.Err() break done default: @@ -358,36 +360,36 @@ done: } } - if err != nil { - log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path) - c <- opts.DecorateLog([]byte("\n" + err.Error() + "\n")) - return err - } - - return nil + return err } -func readLogs(stream io.ReadCloser, c LogChan, opts *LogOptions) { +func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOptions) { defer func() { - log.Debug().Msgf(">>> Closing stream %s", opts.Info()) + log.Debug().Msgf("READ_LOGS BAILED!!!") if err := stream.Close(); err != nil { log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info()) } }() + log.Debug().Msgf("READ_LOGS PROCESSING %#v", opts) r := bufio.NewReader(stream) for { bytes, err := r.ReadBytes('\n') if err != nil { if errors.Is(err, io.EOF) { log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info()) + // c <- ItemEOF return } log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info()) - c <- opts.DecorateLog([]byte(fmt.Sprintf("\nlog stream failed: %#v\n", err))) return } - c <- opts.DecorateLog(bytes) + select { + case c <- opts.DecorateLog(bytes): + case <-ctx.Done(): + log.Debug().Msgf("READER CANCELED") + return + } } } diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index 016a5792..a1681566 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -7,6 +7,7 @@ import ( "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/port" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,8 +29,7 @@ type PortForwarder struct { stopChan, readyChan chan struct{} active bool path string - container string - ports []string + tunnel port.PortTunnel age time.Time } @@ -57,58 +57,56 @@ func (p *PortForwarder) SetActive(b bool) { p.active = b } -// Ports returns the forwarded ports mappings. -func (p *PortForwarder) Ports() []string { - return p.ports +// Port returns the port mapping. +func (p *PortForwarder) Port() string { + return p.tunnel.PortMap() +} + +// ContainerPort returns the container port. +func (p *PortForwarder) ContainerPort() string { + return p.tunnel.ContainerPort +} + +// LocalPort returns the local port. +func (p *PortForwarder) LocalPort() string { + return p.tunnel.LocalPort } // Path returns the pod resource path. func (p *PortForwarder) Path() string { - return PortForwardID(p.path, p.container) + return PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap()) } -// PortForwardID computes port-forward identifier. -func PortForwardID(path, co string) string { - return path + ":" + co +// ID returns a pf id. +func (p *PortForwarder) ID() string { + return PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap()) } // Container returns the target's container. func (p *PortForwarder) Container() string { - return p.container + return p.tunnel.Container } // Stop terminates a port forward. func (p *PortForwarder) Stop() { - log.Debug().Msgf("<<< Stopping PortForward %q %v", p.path, p.ports) + log.Debug().Msgf("<<< Stopping PortForward %s", p.ID()) p.active = false close(p.stopChan) } // FQN returns the portforward unique id. func (p *PortForwarder) FQN() string { - return p.path + ":" + p.container + return p.path + ":" + p.tunnel.Container } // HasPortMapping checks if port mapping is defined for this fwd. -func (p *PortForwarder) HasPortMapping(m string) bool { - for _, mapping := range p.ports { - if mapping == m { - return true - } - } - return false +func (p *PortForwarder) HasPortMapping(portMap string) bool { + return p.tunnel.PortMap() == portMap } // Start initiates a port forward session for a given pod and ports. -func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) { - if len(tt) == 0 { - return nil, fmt.Errorf("no ports assigned") - } - fwds, addrs := make([]string, 0, len(tt)), make([]string, 0, len(tt)) - for _, t := range tt { - fwds, addrs = append(fwds, t.PortMap()), append(addrs, t.Address) - } - p.path, p.container, p.ports, p.age = path, co, fwds, time.Now() +func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.PortForwarder, error) { + p.path, p.tunnel, p.age = path, tt, time.Now() ns, n := client.Namespaced(path) auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb}) @@ -155,10 +153,10 @@ func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portfor Name(n). SubResource("portforward") - return p.forwardPorts("POST", req.URL(), addrs, fwds) + return p.forwardPorts("POST", req.URL(), tt.Address, tt.PortMap()) } -func (p *PortForwarder) forwardPorts(method string, url *url.URL, addrs, ports []string) (*portforward.PortForwarder, error) { +func (p *PortForwarder) forwardPorts(method string, url *url.URL, addr, portMap string) (*portforward.PortForwarder, error) { cfg, err := p.Client().Config().RESTConfig() if err != nil { return nil, err @@ -169,12 +167,17 @@ func (p *PortForwarder) forwardPorts(method string, url *url.URL, addrs, ports [ } dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) - return portforward.NewOnAddresses(dialer, addrs, ports, p.stopChan, p.readyChan, p.Out, p.ErrOut) + return portforward.NewOnAddresses(dialer, []string{addr}, []string{portMap}, p.stopChan, p.readyChan, p.Out, p.ErrOut) } // ---------------------------------------------------------------------------- // Helpers... +// PortForwardID computes port-forward identifier. +func PortForwardID(path, co, portMap string) string { + return path + "|" + co + "|" + portMap +} + func codec() (serializer.CodecFactory, runtime.ParameterCodec) { scheme := runtime.NewScheme() gv := schema.GroupVersion{Group: "", Version: "v1"} diff --git a/internal/dao/registry.go b/internal/dao/registry.go index dc41dc13..00d7ee05 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -341,7 +341,7 @@ func loadRBAC(m ResourceMetas) { func loadPreferred(f Factory, m ResourceMetas) error { if !f.Client().ConnectionOK() { - log.Error().Msgf("PreferredRES - No API server connection") + log.Error().Msgf("Load cluster resources - No API server connection") return nil } @@ -393,6 +393,9 @@ func isDeprecated(gvr client.GVR) bool { } func loadCRDs(f Factory, m ResourceMetas) { + if !f.Client().ConnectionOK() { + return + } const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions" oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything()) if err != nil { diff --git a/internal/dao/sts.go b/internal/dao/sts.go index b483592b..da8bd8fe 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -70,8 +70,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error { return err } - ns, _ := client.Namespaced(path) - auth, err := s.Client().CanI(ns, "apps/v1/statefulsets", []string{client.PatchVerb}) + auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", []string{client.PatchVerb}) if err != nil { return err } @@ -95,6 +94,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error { update, metav1.PatchOptions{}, ) + return err } diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 57164e5d..961cd952 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -55,13 +55,7 @@ func NewClusterMeta() ClusterMeta { // Deltas diffs cluster meta return true if different, false otherwise. func (c ClusterMeta) Deltas(n ClusterMeta) bool { - if c.Cpu != n.Cpu { - return true - } - if c.Mem != n.Mem { - return true - } - if c.Ephemeral != n.Ephemeral { + if c.Cpu != n.Cpu || c.Mem != n.Mem || c.Ephemeral != n.Ephemeral { return true } @@ -76,6 +70,7 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool { // ClusterInfo models cluster metadata. type ClusterInfo struct { cluster *Cluster + factory dao.Factory data ClusterMeta version string listeners []ClusterInfoListener @@ -83,11 +78,12 @@ type ClusterInfo struct { } // NewClusterInfo returns a new instance. -func NewClusterInfo(f dao.Factory, version string) *ClusterInfo { +func NewClusterInfo(f dao.Factory, v string) *ClusterInfo { c := ClusterInfo{ + factory: f, cluster: NewCluster(f), data: NewClusterMeta(), - version: version, + version: v, cache: cache.NewLRUExpireCache(cacheSize), } @@ -119,25 +115,26 @@ func (c *ClusterInfo) Reset(f dao.Factory) { // Refresh fetches the latest cluster meta. func (c *ClusterInfo) Refresh() { data := NewClusterMeta() - data.Context = c.cluster.ContextName() - data.Cluster = c.cluster.ClusterName() - data.User = c.cluster.UserName() + if c.factory.Client().ConnectionOK() { + data.Context = c.cluster.ContextName() + data.Cluster = c.cluster.ClusterName() + data.User = c.cluster.UserName() + data.K8sVer = c.cluster.Version() + ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout()) + defer cancel() + var mx client.ClusterMetrics + if err := c.cluster.Metrics(ctx, &mx); err == nil { + data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral + } else { + log.Warn().Err(err).Msgf("Cluster metrics failed") + } + } data.K9sVer = c.version v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev()) data.K9sVer, data.K9sLatest = v1.String(), v2.String() if v1.IsCurrent(v2) { data.K9sLatest = "" } - data.K8sVer = c.cluster.Version() - - ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout()) - defer cancel() - var mx client.ClusterMetrics - if err := c.cluster.Metrics(ctx, &mx); err == nil { - data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral - } else { - log.Warn().Err(err).Msgf("Cluster metrics failed") - } if c.data.Deltas(data) { c.fireMetaChanged(c.data, data) diff --git a/internal/model/cmd_buff.go b/internal/model/cmd_buff.go index af2074df..b4ff0751 100644 --- a/internal/model/cmd_buff.go +++ b/internal/model/cmd_buff.go @@ -91,8 +91,8 @@ func (c *CmdBuff) Add(r rune) { if c.cancel != nil { return } - var ctx context.Context - ctx, c.cancel = context.WithTimeout(context.Background(), keyEntryDelay) + ctx := context.Background() + ctx, c.cancel = context.WithTimeout(ctx, keyEntryDelay) go func() { <-ctx.Done() @@ -118,8 +118,8 @@ func (c *CmdBuff) Delete() { return } - var ctx context.Context - ctx, c.cancel = context.WithTimeout(context.Background(), 800*time.Millisecond) + ctx := context.Background() + ctx, c.cancel = context.WithTimeout(ctx, 800*time.Millisecond) go func() { <-ctx.Done() diff --git a/internal/model/flash.go b/internal/model/flash.go index d7fe8300..150ae8bc 100644 --- a/internal/model/flash.go +++ b/internal/model/flash.go @@ -126,8 +126,8 @@ func (f *Flash) SetMessage(level FlashLevel, msg string) { f.setLevelMessage(LevelMessage{Level: level, Text: msg}) f.fireFlashChanged() - var ctx context.Context - ctx, f.cancel = context.WithCancel(context.Background()) + ctx := context.Background() + ctx, f.cancel = context.WithCancel(ctx) go f.refresh(ctx) } diff --git a/internal/model/log.go b/internal/model/log.go index 3b40fe0a..8e5d22f4 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -24,12 +24,21 @@ type LogsListener interface { // LogFailed indicates a log failure. LogFailed(error) + + // LogStop indicates logging was canceled. + LogStop() + + // LogResume indicates loggings has resumed. + LogResume() + + // LogCanceled indicates no more logs will come. + LogCanceled() } // Log represents a resource logger. type Log struct { factory dao.Factory - items *dao.LogItems + lines *dao.LogItems listeners []LogsListener gvr client.GVR logOptions *dao.LogOptions @@ -45,11 +54,19 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *L return &Log{ gvr: gvr, logOptions: opts, - items: dao.NewLogItems(), + lines: dao.NewLogItems(), flushTimeout: flushTimeout, } } +func (l *Log) GVR() client.GVR { + return l.gvr +} + +func (l *Log) LogOptions() *dao.LogOptions { + return l.logOptions +} + // SinceSeconds returns since seconds option. func (l *Log) SinceSeconds() int64 { l.mx.RLock() @@ -58,16 +75,33 @@ func (l *Log) SinceSeconds() int64 { return l.logOptions.SinceSeconds } +// IsHead returns log head option. +func (l *Log) IsHead() bool { + l.mx.RLock() + defer l.mx.RUnlock() + + return l.logOptions.Head +} + // ToggleShowTimestamp toggles to logs timestamps. func (l *Log) ToggleShowTimestamp(b bool) { l.logOptions.ShowTimestamp = b l.Refresh() } +func (l *Log) Head(ctx context.Context, c dao.LogChan) { + l.mx.Lock() + { + l.logOptions.Head = true + } + l.mx.Unlock() + l.Restart(ctx, c, true) +} + // SetSinceSeconds sets the logs retrieval time. -func (l *Log) SetSinceSeconds(i int64) { - l.logOptions.SinceSeconds = i - l.Restart() +func (l *Log) SetSinceSeconds(ctx context.Context, c dao.LogChan, i int64) { + l.logOptions.SinceSeconds, l.logOptions.Head = i, false + l.Restart(ctx, c, true) } // Configure sets logger configuration. @@ -100,7 +134,7 @@ func (l *Log) Init(f dao.Factory) { func (l *Log) Clear() { l.mx.Lock() { - l.items.Clear() + l.lines.Clear() l.lastSent = 0 } l.mx.Unlock() @@ -111,21 +145,24 @@ func (l *Log) Clear() { // Refresh refreshes the logs. func (l *Log) Refresh() { l.fireLogCleared() - ll := make([][]byte, l.items.Len()) - l.items.Render(l.logOptions.ShowTimestamp, ll) + ll := make([][]byte, l.lines.Len()) + l.lines.Render(0, l.logOptions.ShowTimestamp, ll) l.fireLogChanged(ll) } // Restart restarts the logger. -func (l *Log) Restart() { - l.Clear() +func (l *Log) Restart(ctx context.Context, c dao.LogChan, clear bool) { l.Stop() - l.Start() + if clear { + l.Clear() + } + l.fireLogResume() + l.Start(ctx, c) } // Start starts logging. -func (l *Log) Start() { - if err := l.load(); err != nil { +func (l *Log) Start(ctx context.Context, c dao.LogChan) { + if err := l.load(ctx, c); err != nil { log.Error().Err(err).Msgf("Tail logs failed!") l.fireLogError(err) } @@ -134,23 +171,20 @@ func (l *Log) Start() { // Stop terminates logging. func (l *Log) Stop() { defer log.Debug().Msgf("<<<< Logger STOPPED!") - if l.cancelFn != nil { - l.cancelFn() - l.cancelFn = nil - } + l.cancel() } // Set sets the log lines (for testing only!) -func (l *Log) Set(items *dao.LogItems) { +func (l *Log) Set(lines *dao.LogItems) { l.mx.Lock() { - l.items.Merge(items) + l.lines.Merge(lines) } l.mx.Unlock() l.fireLogCleared() - ll := make([][]byte, l.items.Len()) - l.items.Render(l.logOptions.ShowTimestamp, ll) + ll := make([][]byte, l.lines.Len()) + l.lines.Render(0, l.logOptions.ShowTimestamp, ll) l.fireLogChanged(ll) } @@ -163,34 +197,31 @@ func (l *Log) ClearFilter() { l.mx.Unlock() l.fireLogCleared() - ll := make([][]byte, l.items.Len()) - l.items.Render(l.logOptions.ShowTimestamp, ll) + ll := make([][]byte, l.lines.Len()) + l.lines.Render(0, l.logOptions.ShowTimestamp, ll) l.fireLogChanged(ll) } // Filter filters the model using either fuzzy or regexp. func (l *Log) Filter(q string) { l.mx.Lock() - defer l.mx.Unlock() - - if len(q) == 0 { - l.filter = "" - l.fireLogCleared() - l.fireLogBuffChanged(l.items) - return + { + l.filter = q } + l.mx.Unlock() - l.filter = q l.fireLogCleared() - l.fireLogBuffChanged(l.items) + l.fireLogBuffChanged(0) } -func (l *Log) load() error { - var ctx context.Context +func (l *Log) load(ctx context.Context, c dao.LogChan) error { + if l.cancelFn != nil { + l.cancelFn() + l.cancelFn = nil + } + ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory) ctx, l.cancelFn = context.WithCancel(ctx) - - c := make(dao.LogChan, 10) go l.updateLogs(ctx, c) accessor, err := dao.AccessorFor(l.factory, l.gvr) @@ -205,40 +236,40 @@ func (l *Log) load() error { go func() { if err = loggable.TailLogs(ctx, c, l.logOptions); err != nil { log.Error().Err(err).Msgf("Tail logs failed") - l.mx.Lock() - if l.cancelFn != nil { - l.cancelFn() - } - l.mx.Unlock() + l.cancel() } }() return nil } +func (l *Log) cancel() { + l.mx.Lock() + { + if l.cancelFn == nil { + l.mx.Unlock() + return + } + l.cancelFn() + l.cancelFn = nil + } + l.mx.Unlock() +} + // Append adds a log line. func (l *Log) Append(line *dao.LogItem) { if line == nil || line.IsEmpty() { return } - l.mx.Lock() - { - l.logOptions.SinceTime = line.Timestamp - } - l.mx.Unlock() - - if l.items.Len() == 0 { - l.fireLogCleared() - } - l.mx.Lock() defer l.mx.Unlock() - if l.items.Len() < int(l.logOptions.Lines) { - l.items.Add(line) + l.logOptions.SinceTime = line.GetTimestamp() + if l.lines.Len() < int(l.logOptions.Lines) { + l.lines.Add(line) return } - l.items.Shift(line) + l.lines.Shift(line) l.lastSent-- if l.lastSent < 0 { l.lastSent = 0 @@ -250,36 +281,40 @@ func (l *Log) Notify() { l.mx.Lock() defer l.mx.Unlock() - if l.lastSent < l.items.Len() { - l.fireLogBuffChanged(l.items.Subset(l.lastSent)) - l.lastSent = l.items.Len() + if l.lastSent < l.lines.Len() { + l.fireLogBuffChanged(l.lastSent) + l.lastSent = l.lines.Len() } } // ToggleAllContainers toggles to show all containers logs. -func (l *Log) ToggleAllContainers() { +func (l *Log) ToggleAllContainers(ctx context.Context, c dao.LogChan) { l.logOptions.ToggleAllContainers() - l.Restart() + l.Restart(ctx, c, true) } func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) { - defer func() { - log.Debug().Msgf("updateLogs view bailing out!") - }() + defer log.Debug().Msgf("updateLogs view bailing out!") + for { select { case item, ok := <-c: if !ok { - log.Debug().Msgf("Closed channel detected. Bailing out...") + log.Debug().Msgf("Closed channel detected. Bailing out!") l.Append(item) l.Notify() return } + if item == dao.ItemEOF { + log.Debug().Msgf("!!!!!GOT EOF!!!!!!") + l.fireCanceled() + return + } l.Append(item) var overflow bool l.mx.RLock() { - overflow = int64(l.items.Len()-l.lastSent) > l.logOptions.Lines + overflow = int64(l.lines.Len()-l.lastSent) > l.logOptions.Lines } l.mx.RUnlock() if overflow { @@ -288,6 +323,7 @@ func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) { case <-time.After(l.flushTimeout): l.Notify() case <-ctx.Done(): + log.Debug().Msgf("!!!LOG_MODEL IS CANCELED!!!") return } } @@ -295,11 +331,17 @@ func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) { // AddListener adds a new model listener. func (l *Log) AddListener(listener LogsListener) { + l.mx.Lock() + defer l.mx.Unlock() + l.listeners = append(l.listeners, listener) } // RemoveListener delete a listener from the list. func (l *Log) RemoveListener(listener LogsListener) { + l.mx.Lock() + defer l.mx.Unlock() + victim := -1 for i, lis := range l.listeners { if lis == listener { @@ -313,19 +355,19 @@ func (l *Log) RemoveListener(listener LogsListener) { } } -func (l *Log) applyFilter(q string) ([][]byte, error) { +func (l *Log) applyFilter(index int, q string) ([][]byte, error) { if q == "" { return nil, nil } - matches, indices, err := l.items.Filter(q, l.logOptions.ShowTimestamp) + matches, indices, err := l.lines.Filter(index, q, l.logOptions.ShowTimestamp) if err != nil { return nil, err } // No filter! if matches == nil { - ll := make([][]byte, l.items.Len()) - l.items.Render(l.logOptions.ShowTimestamp, ll) + ll := make([][]byte, l.lines.Len()) + l.lines.Render(index, l.logOptions.ShowTimestamp, ll) return ll, nil } // Blank filter @@ -333,31 +375,45 @@ func (l *Log) applyFilter(q string) ([][]byte, error) { return nil, nil } filtered := make([][]byte, 0, len(matches)) - lines := l.items.Lines(l.logOptions.ShowTimestamp) + ll := make([][]byte, l.lines.Len()) + l.lines.Lines(index, l.logOptions.ShowTimestamp, ll) for i, idx := range matches { - filtered = append(filtered, color.Highlight(lines[idx], indices[i], 209)) + filtered = append(filtered, color.Highlight(ll[idx], indices[i], 209)) } return filtered, nil } -func (l *Log) fireLogBuffChanged(lines *dao.LogItems) { - ll := make([][]byte, lines.Len()) +func (l *Log) fireLogBuffChanged(index int) { + ll := make([][]byte, l.lines.Len()-index) if l.filter == "" { - lines.Render(l.logOptions.ShowTimestamp, ll) + l.lines.Render(index, l.logOptions.ShowTimestamp, ll) } else { - ff, err := l.applyFilter(l.filter) + ff, err := l.applyFilter(index, l.filter) if err != nil { l.fireLogError(err) return } ll = ff } + if len(ll) > 0 { l.fireLogChanged(ll) } } +func (l *Log) fireLogResume() { + for _, lis := range l.listeners { + lis.LogResume() + } +} + +func (l *Log) fireCanceled() { + for _, lis := range l.listeners { + lis.LogCanceled() + } +} + func (l *Log) fireLogError(err error) { for _, lis := range l.listeners { lis.LogFailed(err) @@ -371,7 +427,13 @@ func (l *Log) fireLogChanged(lines [][]byte) { } func (l *Log) fireLogCleared() { - for _, lis := range l.listeners { + var ll []LogsListener + l.mx.RLock() + { + ll = l.listeners + } + l.mx.RUnlock() + for _, lis := range ll { lis.LogCleared() } } diff --git a/internal/model/log_int_test.go b/internal/model/log_int_test.go index 1bb94ca5..c7d17c65 100644 --- a/internal/model/log_int_test.go +++ b/internal/model/log_int_test.go @@ -19,15 +19,14 @@ func TestUpdateLogs(t *testing.T) { v := newMockLogView() m.AddListener(v) - c := make(dao.LogChan) - go func() { - m.updateLogs(context.Background(), c) - }() + c := make(dao.LogChan, 2) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go m.updateLogs(ctx, c) for i := 0; i < 2*size; i++ { c <- dao.NewLogItemFromString("line" + strconv.Itoa(i)) } - close(c) time.Sleep(2 * time.Second) assert.Equal(t, size, v.count) @@ -45,11 +44,12 @@ func BenchmarkUpdateLogs(b *testing.B) { go func() { m.updateLogs(context.Background(), c) }() + item := dao.NewLogItem([]byte("\033[0;38m2018-12-14T10:36:43.326972-07:00 \033[0;32mblee line")) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - c <- dao.NewLogItemFromString("line" + strconv.Itoa(n)) + c <- item } close(c) } @@ -75,5 +75,8 @@ func newMockLogView() *mockLogView { func (t *mockLogView) LogChanged(ll [][]byte) { t.count += len(ll) } +func (t *mockLogView) LogStop() {} +func (t *mockLogView) LogCanceled() {} +func (t *mockLogView) LogResume() {} func (t *mockLogView) LogCleared() {} func (t *mockLogView) LogFailed(err error) {} diff --git a/internal/model/log_test.go b/internal/model/log_test.go index 06154f09..5ff94f5e 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -1,6 +1,7 @@ package model_test import ( + "context" "fmt" "strconv" "testing" @@ -32,9 +33,8 @@ func TestLogFullBuffer(t *testing.T) { m.Notify() assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 1, v.clearCalled) + assert.Equal(t, 0, v.clearCalled) assert.Equal(t, 0, v.errCalled) - // assert.Equal(t, data.Items()[4:].Lines(false), v.data) } func TestLogFilter(t *testing.T) { @@ -79,13 +79,13 @@ func TestLogFilter(t *testing.T) { m.Notify() assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 2, v.clearCalled) + assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, u.e, len(v.data)) m.ClearFilter() assert.Equal(t, 2, v.dataCalled) - assert.Equal(t, 3, v.clearCalled) + assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, size, len(v.data)) }) @@ -99,7 +99,10 @@ func TestLogStartStop(t *testing.T) { v := newTestView() m.AddListener(v) - m.Start() + c := make(dao.LogChan, 2) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + m.Start(ctx, c) data := dao.NewLogItems() data.Add(dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")) for _, d := range data.Items() { @@ -109,7 +112,7 @@ func TestLogStartStop(t *testing.T) { m.Stop() assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 1, v.clearCalled) + assert.Equal(t, 0, v.clearCalled) assert.Equal(t, 1, v.errCalled) assert.Equal(t, 2, len(v.data)) } @@ -132,7 +135,7 @@ func TestLogClear(t *testing.T) { m.Clear() assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 2, v.clearCalled) + assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, len(v.data)) } @@ -151,7 +154,9 @@ func TestLogBasic(t *testing.T) { assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, data.Lines(false), v.data) + ll := make([][]byte, data.Len()) + data.Lines(0, false, ll) + assert.Equal(t, ll, v.data) } func TestLogAppend(t *testing.T) { @@ -163,7 +168,9 @@ func TestLogAppend(t *testing.T) { items := dao.NewLogItems() items.Add(dao.NewLogItemFromString("blah blah")) m.Set(items) - assert.Equal(t, items.Lines(false), v.data) + ll := make([][]byte, items.Len()) + items.Lines(0, false, ll) + assert.Equal(t, ll, v.data) data := dao.NewLogItems() data.Add( @@ -174,7 +181,9 @@ func TestLogAppend(t *testing.T) { m.Append(d) } assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, items.Lines(false), v.data) + ll = make([][]byte, items.Len()) + items.Lines(0, false, ll) + assert.Equal(t, ll, v.data) m.Notify() assert.Equal(t, 2, v.dataCalled) @@ -203,7 +212,7 @@ func TestLogTimedout(t *testing.T) { } m.Notify() assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 2, v.clearCalled) + assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m" assert.Equal(t, e, string(v.data[0])) @@ -215,9 +224,13 @@ func TestToggleAllContainers(t *testing.T) { m := model.NewLog(client.NewGVR(""), opts, 10*time.Millisecond) m.Init(makeFactory()) assert.Equal(t, "blee", m.GetContainer()) - m.ToggleAllContainers() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + c := make(dao.LogChan, 2) + m.ToggleAllContainers(ctx, c) assert.Equal(t, "", m.GetContainer()) - m.ToggleAllContainers() + m.ToggleAllContainers(ctx, c) assert.Equal(t, "blee", m.GetContainer()) } @@ -245,16 +258,17 @@ func newTestView() *testView { return &testView{} } +func (t *testView) LogCanceled() {} +func (t *testView) LogStop() {} +func (t *testView) LogResume() {} func (t *testView) LogChanged(ll [][]byte) { t.data = ll t.dataCalled++ } - func (t *testView) LogCleared() { t.clearCalled++ t.data = nil } - func (t *testView) LogFailed(err error) { fmt.Println("LogErr", err) t.errCalled++ diff --git a/internal/model/table.go b/internal/model/table.go index 33295521..cd45b048 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -164,7 +164,7 @@ func (t *Table) Peek() render.TableData { } func (t *Table) updater(ctx context.Context) { - defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr) + defer log.Debug().Msgf("TABLE-UPDATER canceled -- %q", t.gvr) bf := backoff.NewExponentialBackOff() bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval diff --git a/internal/port/ann.go b/internal/port/ann.go new file mode 100644 index 00000000..8d7b12b4 --- /dev/null +++ b/internal/port/ann.go @@ -0,0 +1,20 @@ +package port + +import ( + "errors" +) + +type Annotations map[string]string + +func (a Annotations) PreferredPorts(specs ContainerPortSpecs) (PFAnns, error) { + if len(specs) == 0 { + return nil, errors.New("no exposed ports") + } + + value, ok := a[K9sPortForwardsKey] + if !ok { + return PFAnns{specs[0].ToPFAnn()}, nil + } + + return specs.MatchAnnotations(value), nil +} diff --git a/internal/port/ann_test.go b/internal/port/ann_test.go new file mode 100644 index 00000000..23518857 --- /dev/null +++ b/internal/port/ann_test.go @@ -0,0 +1,93 @@ +package port_test + +import ( + "errors" + "testing" + + "github.com/derailed/k9s/internal/port" + "github.com/stretchr/testify/assert" +) + +func TestPreferredPorts(t *testing.T) { + uu := map[string]struct { + anns port.Annotations + specs port.ContainerPortSpecs + err error + e string + }{ + "no-ports": { + anns: port.Annotations{ + port.K9sPortForwardsKey: "c1::4321:p1", + }, + err: errors.New("no exposed ports"), + }, + "no-annotations": { + specs: port.ContainerPortSpecs{ + {Container: "c1", PortName: "p1", PortNum: "1234"}, + }, + e: "c1::1234:p1", + }, + "single-numb": { + anns: port.Annotations{ + port.K9sPortForwardsKey: "c1::4321:1234", + }, + specs: port.ContainerPortSpecs{ + {Container: "c1", PortName: "p1", PortNum: "1234"}, + }, + e: "c1::4321:1234/1234", + }, + "single-same": { + anns: port.Annotations{ + port.K9sPortForwardsKey: "c1::1234", + }, + specs: port.ContainerPortSpecs{ + {Container: "c1", PortName: "p1", PortNum: "1234"}, + }, + e: "c1::1234:1234/1234", + }, + "single-mismatch": { + anns: port.Annotations{ + port.K9sPortForwardsKey: "c2::4321:p1", + }, + specs: port.ContainerPortSpecs{ + {Container: "c1", PortName: "p1", PortNum: "1234"}, + }, + }, + "multi": { + anns: port.Annotations{ + port.K9sPortForwardsKey: "c1::4321:1234,c1::5432:2345", + }, + specs: port.ContainerPortSpecs{ + {Container: "c1", PortName: "p1", PortNum: "1234"}, + {Container: "c1", PortName: "p2", PortNum: "2345"}, + }, + e: "c1::4321:1234/1234,c1::5432:2345/2345", + }, + "multi-mismatch": { + anns: port.Annotations{ + port.K9sPortForwardsKey: "c1::4321:1234,c1::5432:2345", + }, + specs: port.ContainerPortSpecs{ + {Container: "c1", PortName: "p1", PortNum: "1234"}, + {Container: "c2", PortName: "p3", PortNum: "2345"}, + }, + e: "c1::4321:1234/1234", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + anns, err := u.anns.PreferredPorts(u.specs) + assert.Equal(t, u.err, err) + if err != nil { + return + } + pfs, err := port.ParsePFs(u.e) + if err != nil { + pfs = port.PFAnns{} + } + assert.Equal(t, pfs, anns) + }) + } +} diff --git a/internal/port/co_portspec.go b/internal/port/co_portspec.go new file mode 100644 index 00000000..3328f14b --- /dev/null +++ b/internal/port/co_portspec.go @@ -0,0 +1,147 @@ +package port + +import ( + "strconv" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// ContainerPortSpecs represents a container exposed ports. +type ContainerPortSpecs []ContainerPortSpec + +func (c ContainerPortSpecs) Dump() string { + ss := make([]string, 0, len(c)) + for _, spec := range c { + ss = append(ss, spec.String()) + } + + return strings.Join(ss, "\n") +} + +// ToTunnels convert port specs to tunnels. +func (c ContainerPortSpecs) ToTunnels(address string) PortTunnels { + tt := make(PortTunnels, 0, len(c)) + for _, spec := range c { + tt = append(tt, spec.ToTunnel(address)) + } + + return tt +} + +// Find finds a matching container port. +func (c ContainerPortSpecs) Find(pf *PFAnn) (ContainerPortSpec, bool) { + for _, spec := range c { + if spec.Match(pf) { + return spec, true + } + } + + return ContainerPortSpec{}, false +} + +// Match checks if container ports match a pf annotation. +func (c ContainerPortSpecs) Match(pf *PFAnn) bool { + for _, spec := range c { + if spec.Match(pf) { + return true + } + } + + return false +} + +func (c ContainerPortSpecs) MatchAnnotations(s string) PFAnns { + pfs, err := ParsePFs(s) + if err != nil { + return nil + } + + mm := make(PFAnns, 0, len(c)) + for _, pf := range pfs { + if pf.Match(c) { + mm = append(mm, pf) + } + } + + return mm +} + +// FromContainerPorts hydrates from a pod container specification. +func FromContainerPorts(co string, pp []v1.ContainerPort) ContainerPortSpecs { + specs := make(ContainerPortSpecs, 0, len(pp)) + for _, p := range pp { + if p.Protocol != v1.ProtocolTCP { + continue + } + specs = append(specs, NewPortSpec(co, p.Name, p.ContainerPort)) + } + + return specs +} + +// ContainerPortSpec represents a container port specification. +type ContainerPortSpec struct { + Container string + PortName string + PortNum string +} + +// NewPortSpec returns a new instance. +func NewPortSpec(co, portName string, port int32) ContainerPortSpec { + return ContainerPortSpec{ + Container: co, + PortName: portName, + PortNum: strconv.Itoa(int(port)), + } +} + +func (c ContainerPortSpec) ToTunnel(address string) PortTunnel { + return PortTunnel{ + Address: address, + LocalPort: c.PortNum, + ContainerPort: c.PortNum, + } +} + +func (c ContainerPortSpec) Port() intstr.IntOrString { + if c.PortName != "" { + return intstr.Parse(c.PortName) + } + + return intstr.Parse(c.PortNum) +} + +func (c ContainerPortSpec) ToPFAnn() *PFAnn { + return &PFAnn{ + Container: c.Container, + ContainerPort: c.Port(), + LocalPort: c.PortNum, + } +} + +// Match checks if the container spec matches an annotation. +func (c ContainerPortSpec) Match(ann *PFAnn) bool { + if c.Container != ann.Container { + return false + } + + switch ann.ContainerPort.Type { + case intstr.String: + return c.PortName == ann.ContainerPort.String() + case intstr.Int: + return c.PortNum == ann.ContainerPort.String() + default: + return false + } +} + +// String dumps spec to string. +func (c ContainerPortSpec) String() string { + s := c.Container + "::" + c.PortNum + if c.PortName != "" { + s += "(" + c.PortName + ")" + } + return s +} diff --git a/internal/port/co_portspec_test.go b/internal/port/co_portspec_test.go new file mode 100644 index 00000000..3886477a --- /dev/null +++ b/internal/port/co_portspec_test.go @@ -0,0 +1,138 @@ +package port_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/port" + "github.com/stretchr/testify/assert" +) + +func TestContainerPortSpecMatch(t *testing.T) { + uu := map[string]struct { + ann string + spec port.ContainerPortSpec + e bool + }{ + "full": { + ann: "c1::4321:1234", + spec: port.ContainerPortSpec{ + Container: "c1", + PortNum: "1234", + }, + e: true, + }, + "no-port-name": { + ann: "c1::4321:p1/1234", + spec: port.ContainerPortSpec{ + Container: "c1", + PortName: "p1", + PortNum: "1234", + }, + e: true, + }, + "port-name-hosed": { + ann: "c1::4321:blee/1234", + spec: port.ContainerPortSpec{ + Container: "c1", + PortName: "fred", + PortNum: "1234", + }, + }, + "container-name-hosed": { + ann: "c2::4321:fred/1234", + spec: port.ContainerPortSpec{ + Container: "c1", + PortName: "blee", + PortNum: "1234", + }, + }, + "port-num-hosed": { + ann: "c2::4321:1235", + spec: port.ContainerPortSpec{ + Container: "c1", + PortNum: "1234", + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pf, err := port.ParsePF(u.ann) + assert.Nil(t, err) + + assert.Equal(t, u.e, u.spec.Match(pf)) + }) + } +} + +func TestContainerPortSpecString(t *testing.T) { + uu := map[string]struct { + spec port.ContainerPortSpec + e string + }{ + "full": { + spec: port.NewPortSpec("c1", "p1", 1234), + e: "c1::1234(p1)", + }, + "no-name": { + spec: port.NewPortSpec("c1", "", 1234), + e: "c1::1234", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.spec.String()) + }) + } +} + +func TestContainerPortSpecsMatch(t *testing.T) { + uu := map[string]struct { + ann string + specs port.ContainerPortSpecs + e bool + }{ + "full": { + ann: "c1::4321:p1", + specs: port.ContainerPortSpecs{ + port.NewPortSpec("c1", "p1", 1234), + port.NewPortSpec("c2", "p2", 1235), + }, + e: true, + }, + "no-name": { + ann: "c1::4321", + specs: port.ContainerPortSpecs{ + port.NewPortSpec("c1", "", 4321), + port.NewPortSpec("c2", "p2", 1235), + }, + e: true, + }, + "name-hosed": { + ann: "c1::4321:p4", + specs: port.ContainerPortSpecs{ + port.NewPortSpec("c1", "p1", 1234), + port.NewPortSpec("c2", "p2", 1235), + }, + }, + "numb-hosed": { + ann: "c1::4321:1235", + specs: port.ContainerPortSpecs{ + port.NewPortSpec("c1", "p1", 1234), + port.NewPortSpec("c2", "p2", 1236), + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pf, err := port.ParsePF(u.ann) + assert.Nil(t, err) + assert.Equal(t, u.e, u.specs.Match(pf)) + }) + } +} diff --git a/internal/port/pf.go b/internal/port/pf.go new file mode 100644 index 00000000..96ac3d3f --- /dev/null +++ b/internal/port/pf.go @@ -0,0 +1,102 @@ +package port + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + // K9sAutoPortForwardKey represents an auto portforwards annotation. + K9sAutoPortForwardsKey = "k9scli.io/auto-portforwards" + + // K9sPortForwardKey represents a portforwards annotation. + K9sPortForwardsKey = "k9scli.io/portforwards" +) + +var pfRX = regexp.MustCompile(`\A([\w-]+)::(\d*):?(\d*|[\w-]*)/?(\d+)?\z`) + +// PFAnn represents a portforward annotation value. +// Shape: container/portname|portNum:localPort +type PFAnn struct { + Container string + ContainerPort intstr.IntOrString + LocalPort string + containerPortNum string +} + +// ParsePF hydrate a portforward annotation from string. +func ParsePF(ann string) (*PFAnn, error) { + var pf PFAnn + r := pfRX.FindStringSubmatch(strings.TrimSpace(ann)) + if len(r) < 4 { + return &pf, fmt.Errorf("invalid pf annotation %s", ann) + } + pf.Container = r[1] + pf.LocalPort, pf.ContainerPort = r[2], intstr.Parse(r[3]) + if r[3] == "" { + pf.ContainerPort = intstr.Parse(pf.LocalPort) + } + + // Testing only! + if len(r) == 5 && r[4] != "" { + pf.containerPortNum = r[4] + } + if pf.LocalPort == "" { + pf.LocalPort = pf.containerPortNum + } + + return &pf, nil +} + +// Match checks if annotation matches any of the container ports. +func (p *PFAnn) Match(ss ContainerPortSpecs) bool { + for _, s := range ss { + if s.Match(p) { + p.containerPortNum = s.PortNum + return true + } + } + + return false +} + +func (p *PFAnn) AsSpec() string { + s := p.Container + "::" + if p.containerPortNum != "" { + return s + p.containerPortNum + } + return s + p.LocalPort +} + +// String dumps the annotation. +func (p *PFAnn) String() string { + return p.Container + "::" + p.LocalPort + ":" + p.containerPortNum +} + +func (p *PFAnn) PortNum() (string, error) { + if p.ContainerPort.Type == intstr.Int { + return p.ContainerPort.String(), nil + } + if p.containerPortNum != "" { + return p.containerPortNum, nil + } + + return "", errors.New("no port number assigned") +} + +func (p *PFAnn) ToTunnel(address string) (PortTunnel, error) { + var pt PortTunnel + port, err := p.PortNum() + if err != nil { + return pt, err + } + + pt.Address, pt.Container = address, p.Container + pt.ContainerPort, pt.LocalPort = port, p.LocalPort + + return pt, nil +} diff --git a/internal/port/pf_test.go b/internal/port/pf_test.go new file mode 100644 index 00000000..0505af17 --- /dev/null +++ b/internal/port/pf_test.go @@ -0,0 +1,207 @@ +package port_test + +import ( + "errors" + "testing" + + "github.com/derailed/k9s/internal/port" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestParsePF(t *testing.T) { + uu := map[string]struct { + exp string + container string + containerPort intstr.IntOrString + localPort string + e error + }{ + "full-numbs": { + exp: "c1::4321:1234", + container: "c1", + containerPort: intstr.Parse("1234"), + localPort: "4321", + }, + "full-named": { + exp: "c1::4321:p1/1234", + container: "c1", + containerPort: intstr.Parse("p1"), + localPort: "4321", + }, + "just-named": { + exp: "c1::p1/1234", + container: "c1", + containerPort: intstr.Parse("p1"), + localPort: "1234", + }, + "just-num": { + exp: "c1::1234", + container: "c1", + containerPort: intstr.Parse("1234"), + localPort: "1234", + }, + "toast": { + exp: "c1:4321:1234", + e: errors.New("invalid pf annotation c1:4321:1234"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pf, err := port.ParsePF(u.exp) + assert.Equal(t, u.e, err) + if err != nil { + return + } + assert.Equal(t, u.container, pf.Container) + assert.Equal(t, u.containerPort, pf.ContainerPort) + assert.Equal(t, u.localPort, pf.LocalPort) + }) + } +} + +func TestPFMatch(t *testing.T) { + uu := map[string]struct { + exp string + specs port.ContainerPortSpecs + err error + e bool + }{ + "match": { + exp: "c1::1234", + specs: port.ContainerPortSpecs{ + {Container: "c1", PortNum: "1234"}, + }, + e: true, + }, + "match-portnum": { + exp: "c1::4321:1234", + specs: port.ContainerPortSpecs{ + {Container: "c1", PortNum: "1234"}, + }, + e: true, + }, + "no-match": { + exp: "c1::1235", + specs: port.ContainerPortSpecs{ + {Container: "c1", PortNum: "1234"}, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pf, err := port.ParsePF(u.exp) + assert.Equal(t, u.err, err) + if err != nil { + return + } + assert.Equal(t, u.e, pf.Match(u.specs)) + }) + } +} + +func TestPFPortNum(t *testing.T) { + uu := map[string]struct { + exp string + err error + e string + }{ + "port-name": { + exp: "c1::4321:1234", + e: "1234", + }, + "port-number": { + exp: "c1::4321:1234", + e: "1234", + }, + "missing-port-number": { + exp: "c1::p1", + err: errors.New("no port number assigned"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pf, err := port.ParsePF(u.exp) + assert.Nil(t, err) + n, err := pf.PortNum() + assert.Equal(t, u.err, err) + if err != nil { + return + } + assert.Equal(t, u.e, n) + }) + } +} + +func TestPFToTunnel(t *testing.T) { + uu := map[string]struct { + exp string + err error + e port.PortTunnel + }{ + "port-name": { + exp: "c1::p1/1234", + e: port.PortTunnel{ + Address: "blee", + Container: "c1", + LocalPort: "1234", + ContainerPort: "1234", + }, + }, + "port-numb": { + exp: "c1::4321:1234", + e: port.PortTunnel{ + Address: "blee", + Container: "c1", + LocalPort: "4321", + ContainerPort: "1234", + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pf, err := port.ParsePF(u.exp) + assert.Nil(t, err) + pt, err := pf.ToTunnel("blee") + assert.Equal(t, u.err, err) + if err != nil { + return + } + assert.Equal(t, u.e, pt) + }) + } +} + +func TestPFString(t *testing.T) { + uu := map[string]struct { + exp string + err error + e string + }{ + "port-name": { + exp: "c1::p1/1234", + e: "c1::1234:1234", + }, + "port-numb": { + exp: "c1::4321:1234/1234", + e: "c1::4321:1234", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pf, err := port.ParsePF(u.exp) + assert.Nil(t, err) + assert.Equal(t, u.e, pf.String()) + }) + } +} diff --git a/internal/port/pfs.go b/internal/port/pfs.go new file mode 100644 index 00000000..70509d01 --- /dev/null +++ b/internal/port/pfs.go @@ -0,0 +1,92 @@ +package port + +import ( + "fmt" + "strings" +) + +// PortCheck checks if port is free on host. +type PortChecker func(PortTunnel) bool + +// PFAnns represents a collection of port forward annotations. +type PFAnns []*PFAnn + +// ToPortSpec returns a container port and local port definitions. +func (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (string, string) { + specs, lps := make([]string, 0, len(aa)), make([]string, 0, len(aa)) + for _, a := range aa { + specs = append(specs, a.AsSpec()) + if a.LocalPort == "" { + if spec, ok := pp.Find(a); ok { + a.LocalPort = spec.PortNum + } + } + if a.LocalPort != "" { + lps = append(lps, a.LocalPort) + } + } + + return strings.Join(specs, ","), strings.Join(lps, ",") +} + +func (aa PFAnns) ToTunnels(address string, pp ContainerPortSpecs, available PortChecker) (PortTunnels, error) { + pts := make(PortTunnels, 0, len(aa)) + for _, a := range aa { + if !a.Match(pp) { + return nil, fmt.Errorf("ann does not match container port specs") + } + pt, err := a.ToTunnel(address) + if err != nil { + return pts, err + } + if !available(pt) { + return pts, fmt.Errorf("Port %s is not available on host", pt.LocalPort) + } + pts = append(pts, pt) + } + + return pts, nil +} + +// ParsePFs hydrates a collection of portforward annotations. +func ParsePFs(ann string) (PFAnns, error) { + ss := strings.Split(ann, ",") + pp := make(PFAnns, 0, len(ss)) + for _, s := range ss { + f, err := ParsePF(s) + if err != nil { + return nil, err + } + pp = append(pp, f) + } + + return pp, nil +} + +func ToTunnels(address, specs, localPorts string) (PortTunnels, error) { + pp, lps := strings.Split(specs, ","), strings.Split(localPorts, ",") + + if len(pp) != len(lps) { + return nil, fmt.Errorf("spec to local port count mismatch. Expected %d but got %d", len(pp), len(lps)) + } + + pts := make(PortTunnels, 0, len(pp)) + for i, p := range pp { + a, err := ParsePF(p) + if err != nil { + return nil, err + } + n, err := a.PortNum() + if err != nil { + return nil, err + } + pts = append(pts, PortTunnel{ + Address: address, + Container: a.Container, + ContainerPort: n, + LocalPort: lps[i], + }) + } + + return pts, nil +} diff --git a/internal/port/pfs_test.go b/internal/port/pfs_test.go new file mode 100644 index 00000000..200ed451 --- /dev/null +++ b/internal/port/pfs_test.go @@ -0,0 +1,185 @@ +package port_test + +import ( + "errors" + "testing" + + "github.com/derailed/k9s/internal/port" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestParsePFs(t *testing.T) { + uu := map[string]struct { + exp string + pfs port.PFAnns + e error + }{ + "single": { + exp: "c2::4321:1234", + pfs: port.PFAnns{ + {Container: "c2", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, + }, + }, + "multi": { + exp: "c1::4321:1234,c2::6666:6543", + pfs: port.PFAnns{ + {Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, + {Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"}, + }, + }, + "spaces": { + exp: " c1::4321:1234 , c2::6666:6543 ", + pfs: port.PFAnns{ + {Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, + {Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"}, + }, + }, + "toast": { + exp: "c1::p1:1234,c2::4321", + e: errors.New("invalid pf annotation c1::p1:1234"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pfs, err := port.ParsePFs(u.exp) + assert.Equal(t, u.e, err) + if err != nil { + return + } + assert.Equal(t, u.pfs, pfs) + }) + } +} + +func TestPFsToTunnel(t *testing.T) { + uu := map[string]struct { + exp string + specs port.ContainerPortSpecs + pts port.PortTunnels + e error + }{ + "single": { + exp: "c2::4321:1234", + specs: port.ContainerPortSpecs{ + {Container: "c2", PortName: "p1", PortNum: "1234"}, + }, + pts: port.PortTunnels{ + {Address: "fred", Container: "c2", ContainerPort: "1234", LocalPort: "4321"}, + }, + }, + "hosed": { + exp: "c2::p2", + specs: port.ContainerPortSpecs{ + {Container: "c2", PortName: "p1", PortNum: "1234"}, + }, + pts: port.PortTunnels{ + {Address: "fred", Container: "c2", ContainerPort: "1234", LocalPort: "4321"}, + }, + e: errors.New("ann does not match container port specs"), + }, + } + + f := func(port.PortTunnel) bool { + return true + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pfs, err := port.ParsePFs(u.exp) + assert.Nil(t, err) + pts, err := pfs.ToTunnels("fred", u.specs, f) + assert.Equal(t, u.e, err) + if err != nil { + return + } + assert.Equal(t, u.pts, pts) + }) + } +} + +func TestPFsToPortSpec(t *testing.T) { + uu := map[string]struct { + exp string + spec, port string + specs port.ContainerPortSpecs + e error + }{ + "single": { + exp: "c2::4321:p2/1234", + spec: "c2::1234", + port: "4321", + specs: port.ContainerPortSpecs{ + {Container: "c2", PortNum: "1234"}, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + pfs, err := port.ParsePFs(u.exp) + assert.Equal(t, u.e, err) + if err != nil { + return + } + spec, port := pfs.ToPortSpec(u.specs) + assert.Equal(t, u.spec, spec) + assert.Equal(t, u.port, port) + }) + } +} + +func TestToTunnels(t *testing.T) { + uu := map[string]struct { + specs, ports string + tunnels port.PortTunnels + err error + }{ + "single": { + specs: "c2::4321:p2/1234", + ports: "4321", + tunnels: port.PortTunnels{ + { + Address: "blee", + LocalPort: "4321", + Container: "c2", + ContainerPort: "1234", + }, + }, + }, + "multi": { + specs: "c1::5432:2345/2345,c2::4321:p2/1234", + ports: "5432,4321", + tunnels: port.PortTunnels{ + { + Address: "blee", + LocalPort: "5432", + Container: "c1", + ContainerPort: "2345", + }, + { + Address: "blee", + LocalPort: "4321", + Container: "c2", + ContainerPort: "1234", + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + tt, err := port.ToTunnels("blee", u.specs, u.ports) + assert.Equal(t, u.err, err) + if err != nil { + return + } + assert.Equal(t, u.tunnels, tt) + }) + } +} diff --git a/internal/port/tunnel.go b/internal/port/tunnel.go new file mode 100644 index 00000000..99b56281 --- /dev/null +++ b/internal/port/tunnel.go @@ -0,0 +1,49 @@ +package port + +import ( + "fmt" + "net" +) + +// PortTunnels represents a collection of tunnels. +type PortTunnels []PortTunnel + +func (t PortTunnels) CheckAvailable() error { + for _, pt := range t { + if !IsPortFree(pt) { + return fmt.Errorf("port %s is not available on host", pt.LocalPort) + } + } + + return nil +} + +// PortTunnel represents a host tunnel port mapper. +type PortTunnel struct { + Address, Container, LocalPort, ContainerPort string +} + +func NewPortTunnel(a, co, lp, cp string) PortTunnel { + return PortTunnel{ + Address: a, + Container: co, + LocalPort: lp, + ContainerPort: cp, + } +} + +// PortMap returns a port mapping. +func (t PortTunnel) PortMap() string { + if t.LocalPort == "" { + t.LocalPort = t.ContainerPort + } + return t.LocalPort + ":" + t.ContainerPort +} + +func IsPortFree(t PortTunnel) bool { + s, err := net.Listen("tcp", fmt.Sprintf("%s:%s", t.Address, t.LocalPort)) + if err != nil { + return false + } + return s.Close() == nil +} diff --git a/internal/port/tunnel_test.go b/internal/port/tunnel_test.go new file mode 100644 index 00000000..d88dab58 --- /dev/null +++ b/internal/port/tunnel_test.go @@ -0,0 +1,32 @@ +package port_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/port" + "github.com/stretchr/testify/assert" +) + +func TestPortTunnelMap(t *testing.T) { + uu := map[string]struct { + pt port.PortTunnel + coPort, locPort string + e string + }{ + "plain": { + pt: port.PortTunnel{ + Address: "localhost", + LocalPort: "1234", + ContainerPort: "4321", + }, + e: "1234:4321", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.pt.PortMap()) + }) + } +} diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go index 81c9d7cf..3e422c15 100644 --- a/internal/render/port_forward_test.go +++ b/internal/render/port_forward_test.go @@ -26,7 +26,7 @@ func TestPortForwardRender(t *testing.T) { "blee", "fred", "co", - "p1", + "p1:p2", "http://0.0.0.0:p1/", "1", "1", @@ -47,8 +47,8 @@ func (f fwd) Container() string { return "co" } -func (f fwd) Ports() []string { - return []string{"p1"} +func (f fwd) Port() string { + return "p1:p2" } func (f fwd) Active() bool { diff --git a/internal/render/portforward.go b/internal/render/portforward.go index 75a331c1..4f08a3f7 100644 --- a/internal/render/portforward.go +++ b/internal/render/portforward.go @@ -19,7 +19,7 @@ type Forwarder interface { Container() string // Ports returns container exposed ports. - Ports() []string + Port() string // Active returns forwarder current state. Active() bool @@ -60,7 +60,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { return fmt.Errorf("expecting a ForwardRes but got %T", o) } - ports := strings.Split(pf.Ports()[0], ":") + ports := strings.Split(pf.Port(), ":") ns, n := client.Namespaced(pf.Path()) r.ID = pf.Path() @@ -68,7 +68,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { ns, trimContainer(n), pf.Container(), - strings.Join(pf.Ports(), ","), + pf.Port(), UrlFor(pf.Config.Host, pf.Config.Path, ports[0]), AsThousands(int64(pf.Config.C)), AsThousands(int64(pf.Config.N)), @@ -82,11 +82,13 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { // Helpers... func trimContainer(n string) string { - tokens := strings.Split(n, ":") + tokens := strings.Split(n, "|") if len(tokens) == 0 { return n } - return tokens[0] + _, name := client.Namespaced(tokens[0]) + + return name } // UrlFor computes fq url for a given benchmark configuration. diff --git a/internal/ui/app.go b/internal/ui/app.go index 39f57e12..8a2cfb8e 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -41,7 +41,7 @@ func NewApp(cfg *config.Config, context string) *App { a.views = map[string]tview.Primitive{ "menu": NewMenu(a.Styles), "logo": NewLogo(a.Styles), - "prompt": NewPrompt(a.Config.K9s.NoIcons, a.Styles), + "prompt": NewPrompt(&a, a.Config.K9s.NoIcons, a.Styles), "crumbs": NewCrumbs(a.Styles), } @@ -60,6 +60,9 @@ func (a *App) Init() { // QueueUpdate queues up a ui action. func (a *App) QueueUpdate(f func()) { + if a.Application == nil { + return + } go func() { a.Application.QueueUpdate(f) }() @@ -67,6 +70,9 @@ func (a *App) QueueUpdate(f func()) { // QueueUpdateDraw queues up a ui action and redraw the ui. func (a *App) QueueUpdateDraw(f func()) { + if a.Application == nil { + return + } go func() { a.Application.QueueUpdateDraw(f) }() diff --git a/internal/ui/config.go b/internal/ui/config.go index b1044720..23a6c195 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -50,7 +50,7 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e log.Warn().Err(err).Msg("CustomView watcher failed") return case <-ctx.Done(): - log.Debug().Msgf("CustomViewWatcher Done `%s!!", config.K9sViewConfigFile) + log.Debug().Msgf("CustomViewWatcher CANCELED `%s!!", config.K9sViewConfigFile) if err := w.Close(); err != nil { log.Error().Err(err).Msg("Closing CustomView watcher") } @@ -102,7 +102,7 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error log.Info().Err(err).Msg("Skin watcher failed") return case <-ctx.Done(): - log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile) + log.Debug().Msgf("SkinWatcher CANCELED `%s!!", c.skinFile) if err := w.Close(); err != nil { log.Error().Err(err).Msg("Closing Skin watcher") } diff --git a/internal/ui/dialog/delete.go b/internal/ui/dialog/delete.go index 169270bd..b107560b 100644 --- a/internal/ui/dialog/delete.go +++ b/internal/ui/dialog/delete.go @@ -23,10 +23,10 @@ func ShowDelete(styles config.Dialog, pages *ui.Pages, msg string, ok okFunc, ca SetButtonTextColor(styles.ButtonFgColor.Color()). SetLabelColor(styles.LabelFgColor.Color()). SetFieldTextColor(styles.FieldFgColor.Color()) - f.AddCheckbox("Cascade:", cascade, func(checked bool) { + f.AddCheckbox("Cascade:", cascade, func(_ string, checked bool) { cascade = checked }) - f.AddCheckbox("Force:", force, func(checked bool) { + f.AddCheckbox("Force:", force, func(_ string, checked bool) { force = checked }) f.AddButton("Cancel", func() { diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 1c2f64a6..ba962742 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -52,7 +52,7 @@ func (f *Flash) StylesChanged(s *config.Styles) { // Watch watches for flash changes. func (f *Flash) Watch(ctx context.Context, c model.FlashChan) { - defer log.Debug().Msgf("Flash Canceled!") + defer log.Debug().Msgf("Flash Watch Canceled!") for { select { case <-ctx.Done(): diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index b5e5d35f..70362090 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -71,6 +71,7 @@ type PromptModel interface { type Prompt struct { *tview.TextView + app *App noIcons bool icon rune styles *config.Styles @@ -79,8 +80,9 @@ type Prompt struct { } // NewPrompt returns a new command view. -func NewPrompt(noIcons bool, styles *config.Styles) *Prompt { +func NewPrompt(app *App, noIcons bool, styles *config.Styles) *Prompt { p := Prompt{ + app: app, styles: styles, noIcons: noIcons, TextView: tview.NewTextView(), @@ -183,8 +185,15 @@ func (p *Prompt) activate() { } func (p *Prompt) update(s string) { - p.Clear() - p.write(s, "") + f := func() { + p.Clear() + p.write(s, "") + } + if p.app == nil { + f() + return + } + p.app.QueueUpdate(f) } func (p *Prompt) suggest(text, suggestion string) { diff --git a/internal/ui/prompt_test.go b/internal/ui/prompt_test.go index e381cc85..2b8238ab 100644 --- a/internal/ui/prompt_test.go +++ b/internal/ui/prompt_test.go @@ -10,7 +10,7 @@ import ( ) func TestCmdNew(t *testing.T) { - v := ui.NewPrompt(true, config.NewStyles()) + v := ui.NewPrompt(nil, true, config.NewStyles()) model := model.NewFishBuff(':', model.CommandBuffer) v.SetModel(model) model.AddListener(v) @@ -23,7 +23,7 @@ func TestCmdNew(t *testing.T) { func TestCmdUpdate(t *testing.T) { model := model.NewFishBuff(':', model.CommandBuffer) - v := ui.NewPrompt(true, config.NewStyles()) + v := ui.NewPrompt(nil, true, config.NewStyles()) v.SetModel(model) model.AddListener(v) @@ -36,7 +36,7 @@ func TestCmdUpdate(t *testing.T) { func TestCmdMode(t *testing.T) { model := model.NewFishBuff(':', model.CommandBuffer) - v := ui.NewPrompt(true, config.NewStyles()) + v := ui.NewPrompt(&ui.App{}, true, config.NewStyles()) v.SetModel(model) model.AddListener(v) diff --git a/internal/view/actions.go b/internal/view/actions.go index e81de4f3..943c9747 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -136,6 +136,7 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { clear: true, binary: p.Command, background: p.Background, + pipes: p.Pipes, args: args, } if run(r.App(), opts) { diff --git a/internal/view/app.go b/internal/view/app.go index aabc6280..c295b60a 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/signal" + "runtime" "sort" "strings" "sync/atomic" @@ -107,8 +108,10 @@ func (a *App) Init(version string, rate int) error { a.clusterModel = model.NewClusterInfo(a.factory, a.version) a.clusterModel.AddListener(a.clusterInfo()) a.clusterModel.AddListener(a.statusIndicator()) - a.clusterModel.Refresh() - a.clusterInfo().Init() + if a.Conn().ConnectionOK() { + a.clusterModel.Refresh() + a.clusterInfo().Init() + } a.command = NewCommand(a) if err := a.command.Init(); err != nil { @@ -185,6 +188,7 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (a *App) bindKeys() { a.AddActions(ui.KeyActions{ + ui.KeyShiftG: ui.NewSharedKeyAction("DumpGOR", a.dumpGOR, false), tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false), tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false), ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false), @@ -193,6 +197,13 @@ func (a *App) bindKeys() { }) } +func (a *App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey { + bb := make([]byte, 5_000_000) + runtime.Stack(bb, true) + log.Debug().Msgf("GOR\n%s", string(bb)) + return evt +} + // ActiveView returns the currently active view. func (a *App) ActiveView() model.Component { return a.Content.GetPrimitive("main").(model.Component) diff --git a/internal/view/app_test.go b/internal/view/app_test.go index e2b7470a..4ff96086 100644 --- a/internal/view/app_test.go +++ b/internal/view/app_test.go @@ -12,5 +12,5 @@ func TestAppNew(t *testing.T) { a := view.NewApp(config.NewConfig(ks{})) a.Init("blee", 10) - assert.Equal(t, 10, len(a.GetActions())) + assert.Equal(t, 11, len(a.GetActions())) } diff --git a/internal/view/browser.go b/internal/view/browser.go index 72b62940..133e7a82 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -7,6 +7,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/derailed/k9s/internal" @@ -31,6 +32,7 @@ type Browser struct { accessor dao.Accessor contextFn ContextFunc cancelFn context.CancelFunc + mx sync.RWMutex } // NewBrowser returns a new browser. @@ -140,10 +142,14 @@ func (b *Browser) Start() { // Stop terminates browser updates. func (b *Browser) Stop() { - if b.cancelFn != nil { - b.cancelFn() - b.cancelFn = nil + b.mx.Lock() + { + if b.cancelFn != nil { + b.cancelFn() + b.cancelFn = nil + } } + b.mx.Unlock() b.GetModel().RemoveListener(b) b.CmdBuff().RemoveListener(b) b.Table.Stop() @@ -213,7 +219,12 @@ func (b *Browser) Aliases() []string { // TableDataChanged notifies view new data is available. func (b *Browser) TableDataChanged(data render.TableData) { - if !b.app.ConOK() || b.cancelFn == nil || !b.app.IsRunning() { + var cancel context.CancelFunc + b.mx.RLock() + cancel = b.cancelFn + b.mx.RUnlock() + + if !b.app.ConOK() || cancel == nil || !b.app.IsRunning() { return } diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index 6baaefa7..ae047759 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -2,6 +2,7 @@ package view import ( "fmt" + "runtime" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" @@ -102,7 +103,7 @@ func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { c.app.QueueUpdateDraw(func() { c.Clear() c.layout() - row := c.setCell(0, curr.Context) + row := c.setCell(0, fmt.Sprintf("%s [%d]", curr.Context, runtime.NumGoroutine())) row = c.setCell(row, curr.Cluster) row = c.setCell(row, curr.User) if curr.K9sLatest != "" { diff --git a/internal/view/container.go b/internal/view/container.go index 33023d46..c111949f 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -4,11 +4,11 @@ import ( "context" "errors" "fmt" - "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell/v2" @@ -178,89 +178,65 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - ports, ok := c.isForwardable(path) + ports, ann, ok := c.listForwardable(path) if !ok { return nil } - ShowPortForwards(c, c.GetTable().Path, ports, "", startFwdCB) + ShowPortForwards(c, c.GetTable().Path, ports, ann, startFwdCB) return nil } -func (c *Container) isForwardable(path string) ([]string, bool) { - po, err := fetchPod(c.App().factory, c.GetTable().Path) - if err != nil { - return nil, false - } - - var co *v1.Container - cc := po.Spec.Containers - for i := range cc { - if cc[i].Name == path { - co = &cc[i] - } - } - if co == nil { - log.Error().Err(fmt.Errorf("unable to locate container named %q", path)) - return nil, false - } - +func checkRunningStatus(co string, ss []v1.ContainerStatus) error { var cs *v1.ContainerStatus - ss := po.Status.ContainerStatuses for i := range ss { - if ss[i].Name == path { + if ss[i].Name == co { cs = &ss[i] + break } } if cs == nil { - log.Error().Err(fmt.Errorf("unable to locate container status for %q", path)) - return nil, false + return fmt.Errorf("unable to locate container status for %q", co) } if render.ToContainerState(cs.State) != "Running" { - c.App().Flash().Err(fmt.Errorf("Container %s is not running?", path)) - return nil, false + return fmt.Errorf("Container %s is not running?", co) } - portC := render.ToContainerPorts(co.Ports) - ports := strings.Split(portC, ",") - if len(ports) == 0 { + return nil +} + +func locateContainer(co string, cc []v1.Container) (*v1.Container, error) { + for i := range cc { + if cc[i].Name == co { + return &cc[i], nil + } + } + return nil, fmt.Errorf("unable to locate container named %q", co) +} + +func (c *Container) listForwardable(path string) (port.ContainerPortSpecs, map[string]string, bool) { + po, err := fetchPod(c.App().factory, c.GetTable().Path) + if err != nil { + return nil, nil, false + } + + co, err := locateContainer(path, po.Spec.Containers) + if err != nil { + c.App().Flash().Err(err) + return nil, nil, false + } + + if err := checkRunningStatus(path, po.Status.ContainerStatuses); err != nil { + c.App().Flash().Err(err) + return nil, nil, false + } + + exposedPorts := port.FromContainerPorts(path, co.Ports) + if len(exposedPorts) == 0 { c.App().Flash().Err(errors.New("Container exposes no ports")) - return nil, false + return nil, nil, false } - pp := make([]string, 0, len(ports)) - container, port, ok := parsePFAnn(po.Annotations[AnnDefaultPF]) - if ok && container == path { - if index := indexOfPort(ports, port); index != -1 { - pp = append(pp, path+"/"+port) - ports = append(ports[:index], ports[index+1:]...) - } - } - - for _, p := range ports { - if !isTCPPort(p) { - continue - } - pp = append(pp, path+"/"+p) - } - if len(pp) == 0 { - c.App().Flash().Err(errors.New("No TCP port available on container")) - return nil, false - } - - return pp, true -} - -func indexOfPort(pp []string, port string) int { - for i, p := range pp { - tokens := strings.Split(p, ":") - if len(tokens) == 2 { - if tokens[0] == port || tokens[1] == port { - return i - } - } - } - - return -1 + return port.FromContainerPorts(path, co.Ports), po.Annotations, true } diff --git a/internal/view/drain_dialog.go b/internal/view/drain_dialog.go index 17040399..76e0f1bf 100644 --- a/internal/view/drain_dialog.go +++ b/internal/view/drain_dialog.go @@ -45,13 +45,13 @@ func ShowDrain(view ResourceViewer, path string, defaults dao.DrainOptions, okFn view.App().Flash().Clear() opts.Timeout = a }) - f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(v bool) { + f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(_ string, v bool) { opts.IgnoreAllDaemonSets = v }) - f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(v bool) { + f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(_ string, v bool) { opts.DeleteEmptyDirData = v }) - f.AddCheckbox("Force:", defaults.Force, func(v bool) { + f.AddCheckbox("Force:", defaults.Force, func(_ string, v bool) { opts.Force = v }) diff --git a/internal/view/exec.go b/internal/view/exec.go index 7bbeb287..82ba29e2 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "io" "os" "os/exec" "os/signal" @@ -32,6 +33,7 @@ const ( type shellOpts struct { clear, background bool + pipes []string binary string banner string args []string @@ -96,40 +98,41 @@ func execute(opts shellOpts) error { } ctx, cancel := context.WithCancel(context.Background()) defer func() { - cancel() - clearScreen() + if !opts.background { + cancel() + clearScreen() + } }() sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - go func() { + go func(cancel context.CancelFunc) { + defer log.Debug().Msgf("SIGNAL_GOR - BAILED!!") select { case <-sigChan: - log.Debug().Msg("Command canceled with signal!") + log.Debug().Msgf("Command canceled with signal!") cancel() case <-ctx.Done(): - return + log.Debug().Msgf("SIGNAL Context CANCELED!") } - }() + }(cancel) - log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " ")) - cmd := exec.Command(opts.binary, opts.args...) + cmds := make([]*exec.Cmd, 0, 1) + cmd := exec.CommandContext(ctx, opts.binary, opts.args...) + log.Debug().Msgf("RUNNING> %s", cmd) + cmds = append(cmds, cmd) - var err error - if opts.background { - err = cmd.Start() - } else { - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr - _, _ = cmd.Stdout.Write([]byte(opts.banner)) - err = cmd.Run() + for _, p := range opts.pipes { + tokens := strings.Split(p, " ") + if len(tokens) < 2 { + continue + } + cmd := exec.CommandContext(ctx, tokens[0], tokens[1:]...) + log.Debug().Msgf("\t| %s", cmd) + cmds = append(cmds, cmd) } - select { - case <-ctx.Done(): - return errors.New("canceled by operator") - default: - return err - } + return pipe(ctx, opts, cmds...) } func runKu(a *App, opts shellOpts) (string, error) { @@ -358,3 +361,58 @@ func asResource(r config.Limits) v1.ResourceRequirements { }, } } + +func pipe(ctx context.Context, opts shellOpts, cmds ...*exec.Cmd) error { + if len(cmds) == 0 { + return nil + } + + if len(cmds) == 1 { + cmd := cmds[0] + if opts.background { + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, log.Logger, log.Logger + return cmd.Start() + } + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + //cmd.SysProcAttr = &syscall.SysProcAttr{ + //// //Setpgid: true, + //// //Setctty: true, + // Foreground: true, + //} + _, _ = cmd.Stdout.Write([]byte(opts.banner)) + + log.Debug().Msgf("Running Start") + err := cmd.Run() + log.Debug().Msgf("Running Done") + return err + // select { + // case <-ctx.Done(): + // return errors.New("canceled by operator") + // default: + // log.Debug().Msgf("PIPE RETURN %s", err) + // return err + // } + } + + last := len(cmds) - 1 + for i := 0; i < len(cmds); i++ { + cmds[i].Stderr = os.Stderr + if i+1 < len(cmds) { + r, w := io.Pipe() + cmds[i].Stdout, cmds[i+1].Stdin = w, r + } + } + cmds[last].Stdout = os.Stdout + + for _, cmd := range cmds { + log.Debug().Msgf("Starting CMD %s", cmd) + if err := cmd.Start(); err != nil { + return err + } + } + + log.Debug().Msgf("WAITING!!!") + err := cmds[len(cmds)-1].Wait() + log.Debug().Msgf("DONE WAITING!!!") + return err +} diff --git a/internal/view/helpers.go b/internal/view/helpers.go index 42a972ad..51eb57df 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -132,7 +132,7 @@ func extractApp(ctx context.Context) (*App, error) { // AsKey maps a string representation of a key to a tcell key. func asKey(key string) (tcell.Key, error) { for k, v := range tcell.KeyNames { - if v == key { + if key == v { return k, nil } } diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go index 4827e418..c08289f1 100644 --- a/internal/view/helpers_test.go +++ b/internal/view/helpers_test.go @@ -25,14 +25,14 @@ func TestParsePFAnn(t *testing.T) { ok bool }{ "named-port": { - ann: "fred:blee", - co: "fred", + ann: "c1:blee", + co: "c1", port: "blee", ok: true, }, "port-num": { - ann: "fred:1234", - co: "fred", + ann: "c1:1234", + co: "c1", port: "1234", ok: true, }, diff --git a/internal/view/log.go b/internal/view/log.go index a878aec1..2c6debcc 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -1,14 +1,13 @@ package view import ( - "bytes" "context" "fmt" "io" "os" "path/filepath" - "runtime" "strings" + "sync" "time" "github.com/atotto/clipboard" @@ -25,7 +24,7 @@ import ( const ( logTitle = "logs" - logMessage = "Waiting for logs..." + logMessage = "Waiting for logs...\n" logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " flushTimeout = 1 * time.Millisecond @@ -35,11 +34,16 @@ const ( type Log struct { *tview.Flex - app *App - logs *Logger - indicator *LogIndicator - ansiWriter io.Writer - model *model.Log + app *App + logs *Logger + indicator *LogIndicator + ansiWriter io.Writer + model *model.Log + cancelFn context.CancelFunc + cancelUpdates bool + mx sync.Mutex + logChan dao.LogChan + follow bool } var _ model.Component = (*Log)(nil) @@ -47,8 +51,10 @@ var _ model.Component = (*Log)(nil) // NewLog returns a new viewer. func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log { l := Log{ - Flex: tview.NewFlex(), - model: model.NewLog(gvr, opts, flushTimeout), + Flex: tview.NewFlex(), + logChan: make(dao.LogChan, 2), + model: model.NewLog(gvr, opts, flushTimeout), + follow: true, } return &l @@ -76,21 +82,18 @@ func (l *Log) Init(ctx context.Context) (err error) { return err } l.logs.SetBorderPadding(0, 0, 1, 1) - l.logs.SetText(logMessage) + l.logs.SetText("[orange::d]" + logMessage) l.logs.SetWrap(l.app.Config.K9s.Logger.TextWrap) - l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize) - l.logs.cmdBuff.AddListener(l) + l.logs.SetMaxLines(l.app.Config.K9s.Logger.BufferSize) l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String()) l.AddItem(l.logs, 0, 1, true) l.bindKeys() l.StylesChanged(l.app.Styles) - l.app.Styles.AddListener(l) l.goFullScreen() l.model.Init(l.app.factory) - l.model.AddListener(l) l.updateTitle() l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime) @@ -103,6 +106,30 @@ func (l *Log) InCmdMode() bool { return l.logs.cmdBuff.InCmdMode() } +// LogCanceled indicates no more logs are coming. +func (l *Log) LogCanceled() { + log.Debug().Msgf("LOGS_CANCELED!!!") + l.Flush([][]byte{[]byte("\n🏁 [red::b]Stream exited! No more logs...")}) +} + +// LogStop disables log flushes. +func (l *Log) LogStop() { + log.Debug().Msgf("LOG_STOP!!!") + l.mx.Lock() + defer l.mx.Unlock() + + l.cancelUpdates = true +} + +// LogResume resume log flushes. +func (l *Log) LogResume() { + l.mx.Lock() + defer l.mx.Unlock() + + log.Debug().Msgf("LOG_RESUME!!!") + l.cancelUpdates = false +} + // LogCleared clears the logs. func (l *Log) LogCleared() { l.app.QueueUpdateDraw(func() { @@ -126,6 +153,9 @@ func (l *Log) LogFailed(err error) { // LogChanged updates the logs. func (l *Log) LogChanged(lines [][]byte) { l.app.QueueUpdateDraw(func() { + if l.logs.GetText(true) == logMessage { + l.logs.Clear() + } l.Flush(lines) }) } @@ -166,15 +196,43 @@ func (l *Log) ExtraHints() map[string]string { return nil } +func (l *Log) getContext() context.Context { + if l.cancelFn != nil { + l.cancelFn() + } + ctx := context.Background() + ctx, l.cancelFn = context.WithCancel(ctx) + return ctx +} + // Start runs the component. func (l *Log) Start() { - l.model.Start() + log.Debug().Msgf("LOG_VIEW STARTED!!") + + l.model.Restart(l.getContext(), l.logChan, true) + l.model.AddListener(l) + l.app.Styles.AddListener(l) + l.logs.cmdBuff.AddListener(l) + l.logs.cmdBuff.AddListener(l.app.Prompt()) + l.updateTitle() } // Stop terminates the component. func (l *Log) Stop() { - l.model.Stop() + log.Debug().Msgf("LOG_VIEW STOPPED!") l.model.RemoveListener(l) + l.model.Stop() + log.Debug().Msgf("CLOSING LOG_CHANNEL!!!") + l.mx.Lock() + { + if l.cancelFn != nil { + l.cancelFn() + l.cancelFn = nil + } + close(l.logChan) + l.logChan = nil + } + l.mx.Unlock() l.app.Styles.RemoveListener(l) l.logs.cmdBuff.RemoveListener(l) l.logs.cmdBuff.RemoveListener(l.app.Prompt()) @@ -185,12 +243,13 @@ func (l *Log) Name() string { return logTitle } func (l *Log) bindKeys() { l.logs.Actions().Set(ui.KeyActions{ - ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true), - ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true), - ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true), - ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true), - ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true), - ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), + ui.Key0: ui.NewKeyAction("tail", l.sinceCmd(-1), true), + ui.Key1: ui.NewKeyAction("head", l.head(), true), + ui.Key2: ui.NewKeyAction("1m", l.sinceCmd(60), true), + ui.Key3: ui.NewKeyAction("5m", l.sinceCmd(5*60), true), + ui.Key4: ui.NewKeyAction("15m", l.sinceCmd(15*60), true), + ui.Key5: ui.NewKeyAction("30m", l.sinceCmd(30*60), true), + ui.Key6: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false), ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true), @@ -242,13 +301,17 @@ func (l *Log) Indicator() *LogIndicator { } func (l *Log) updateTitle() { - sinceSeconds, since := l.model.SinceSeconds(), "all" + sinceSeconds, since := l.model.SinceSeconds(), "tail" if sinceSeconds > 0 && sinceSeconds < 60*60 { since = fmt.Sprintf("%dm", sinceSeconds/60) } if sinceSeconds >= 60*60 { since = fmt.Sprintf("%dh", sinceSeconds/(60*60)) } + if l.model.IsHead() { + since = "head" + } + var title string path, co := l.model.GetPath(), l.model.GetContainer() if co == "" { @@ -274,25 +337,47 @@ var EOL = []byte{'\n'} // Flush write logs to viewer. func (l *Log) Flush(lines [][]byte) { - log.Debug().Msgf("LINES [%d]%d", runtime.NumGoroutine(), len(strings.Split(l.logs.GetText(true), "\n"))) - if !l.indicator.AutoScroll() { + defer func() { + if l.cancelUpdates { + l.cancelUpdates = false + } + }() + + if len(lines) == 0 || !l.indicator.AutoScroll() || l.cancelUpdates { return } - _, _ = l.ansiWriter.Write(EOL) - if _, err := l.ansiWriter.Write(bytes.Join(lines, EOL)); err != nil { - log.Error().Err(err).Msgf("write logs failed") + for i := 0; i < len(lines); i++ { + if l.cancelUpdates { + break + } + _, _ = l.ansiWriter.Write(lines[i]) + } + if l.follow { + l.logs.ScrollToEnd() } - l.logs.ScrollToEnd() - l.indicator.Refresh() } // ---------------------------------------------------------------------------- // Actions()... +func (l *Log) head() func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("!!!!HEAD!!!!") + l.cancelUpdates = true + l.logs.Clear() + l.model.Head(l.getContext(), l.logChan) + l.updateTitle() + + return nil + } +} + func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - l.model.SetSinceSeconds(int64(a)) + l.logs.Clear() + l.model.SetSinceSeconds(l.getContext(), l.logChan, int64(a)) l.updateTitle() + return nil } } @@ -302,7 +387,7 @@ func (l *Log) toggleAllContainers(evt *tcell.EventKey) *tcell.EventKey { return evt } l.indicator.ToggleAllContainers() - l.model.ToggleAllContainers() + l.model.ToggleAllContainers(l.getContext(), l.logChan) l.updateTitle() return nil @@ -322,7 +407,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { // SaveCmd dumps the logs to file. func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { - if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil { + if path, err := saveData(l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true)); err != nil { l.app.Flash().Err(err) } else { l.app.Flash().Infof("Log %s saved successfully!", path) @@ -377,7 +462,9 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey { _, _, w, _ := l.GetRect() - fmt.Fprintf(l.ansiWriter, "\n[white::b]%s[::]", strings.Repeat("─", w-4)) + fmt.Fprintf(l.ansiWriter, "\n[white:-:b]%s[-:-:-]", strings.Repeat("─", w-4)) + l.follow = true + return nil } @@ -388,6 +475,7 @@ func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey { l.indicator.ToggleTimestamp() l.model.ToggleShowTimestamp(l.indicator.showTime) + l.indicator.Refresh() return nil } @@ -399,6 +487,8 @@ func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey { l.indicator.ToggleTextWrap() l.logs.SetWrap(l.indicator.textWrap) + l.indicator.Refresh() + return nil } @@ -409,11 +499,15 @@ func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { } l.indicator.ToggleAutoScroll() - if l.indicator.AutoScroll() { - l.model.Start() - } else { - l.model.Stop() - } + l.follow = l.indicator.AutoScroll() + // if l.indicator.AutoScroll() { + + // // l.model.Restart(l.getContext(), l.logChan, false) + // } else { + // // l.model.Stop() + // } + l.indicator.Refresh() + return nil } @@ -423,6 +517,8 @@ func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey { } l.indicator.ToggleFullScreen() l.goFullScreen() + l.indicator.Refresh() + return nil } diff --git a/internal/view/log_indicator.go b/internal/view/log_indicator.go index a3dd4f0a..959b51aa 100644 --- a/internal/view/log_indicator.go +++ b/internal/view/log_indicator.go @@ -7,17 +7,7 @@ import ( "github.com/derailed/tview" ) -const ( - autoscroll = "Autoscroll" - fullscreen = "FullScreen" - timestamp = "Timestamps" - wrap = "Wrap" - allContainers = "AllContainers" - on = "[limegreen::]On" - off = "[gray::]Off" - spacer = " " - bold = "[-::b]" -) +const spacer = " " // LogIndicator represents a log view indicator. type LogIndicator struct { @@ -25,6 +15,7 @@ type LogIndicator struct { styles *config.Styles scrollStatus int32 + indicator []byte fullScreen bool textWrap bool showTime bool @@ -33,15 +24,16 @@ type LogIndicator struct { } // NewLogIndicator returns a new indicator. -func NewLogIndicator(cfg *config.Config, styles *config.Styles, isContainerLogView bool) *LogIndicator { +func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bool) *LogIndicator { l := LogIndicator{ styles: styles, TextView: tview.NewTextView(), + indicator: make([]byte, 0, 100), scrollStatus: 1, fullScreen: cfg.K9s.Logger.FullScreenLogs, textWrap: cfg.K9s.Logger.TextWrap, showTime: cfg.K9s.Logger.ShowTime, - shouldDisplayAllContainers: isContainerLogView, + shouldDisplayAllContainers: allContainers, } l.StylesChanged(styles) styles.AddListener(&l) @@ -110,24 +102,46 @@ func (l *LogIndicator) ToggleAllContainers() { l.Refresh() } -// Refresh updates the view. -func (l *LogIndicator) Refresh() { +func (l *LogIndicator) reset() { l.Clear() - if l.shouldDisplayAllContainers { - l.update(allContainers, l.allContainers, spacer) - } - l.update(autoscroll, l.AutoScroll(), spacer) - l.update(fullscreen, l.fullScreen, spacer) - l.update(timestamp, l.showTime, spacer) - l.update(wrap, l.textWrap, "") + l.indicator = l.indicator[:0] } -func (l *LogIndicator) update(title string, state bool, padding string) { - bb := []byte(bold + title + ":") - if state { - bb = append(bb, []byte(on)...) - } else { - bb = append(bb, []byte(off)...) +// Refresh updates the view. +func (l *LogIndicator) Refresh() { + l.reset() + + if l.shouldDisplayAllContainers { + if l.allContainers { + l.indicator = append(l.indicator, "[::b]AllContainers:[limegreen::b]On[-::]"+spacer...) + } else { + l.indicator = append(l.indicator, "[::b]AllContainers:[gray::d]Off[-::]"+spacer...) + } } - _, _ = l.Write(append(bb, []byte(padding)...)) + + if l.AutoScroll() { + l.indicator = append(l.indicator, "[::b]Autoscroll:[limegreen::b]On[-::]"+spacer...) + } else { + l.indicator = append(l.indicator, "[::b]Autoscroll:[gray::d]Off[-::]"+spacer...) + } + + if l.FullScreen() { + l.indicator = append(l.indicator, "[::b]FullScreen:[limegreen::b]On[-::]"+spacer...) + } else { + l.indicator = append(l.indicator, "[::b]FullScreen:[gray::d]Off[-::]"+spacer...) + } + + if l.Timestamp() { + l.indicator = append(l.indicator, "[::b]Timestamps:[limegreen::b]On[-::]"+spacer...) + } else { + l.indicator = append(l.indicator, "[::b]Timestamps:[gray::d]Off[-::]"+spacer...) + } + + if l.TextWrap() { + l.indicator = append(l.indicator, "[::b]Wrap:[limegreen::b]On[-::]"...) + } else { + l.indicator = append(l.indicator, "[::b]Wrap:[gray::d]Off[-::]"...) + } + + _, _ = l.Write(l.indicator) } diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go index d4156ab1..92649d7b 100644 --- a/internal/view/log_indicator_test.go +++ b/internal/view/log_indicator_test.go @@ -14,11 +14,11 @@ func TestLogIndicatorRefresh(t *testing.T) { li *view.LogIndicator e string }{ - "all containers": { - view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[-::b]AllContainers:[gray::]Off [-::b]Autoscroll:[limegreen::]On [-::b]FullScreen:[gray::]Off [-::b]Timestamps:[gray::]Off [-::b]Wrap:[gray::]Off\n", + "all-containers": { + view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", }, - "no all containers": { - view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[-::b]Autoscroll:[limegreen::]On [-::b]FullScreen:[gray::]Off [-::b]Timestamps:[gray::]Off [-::b]Wrap:[gray::]Off\n", + "plain": { + view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", }, } @@ -26,7 +26,18 @@ func TestLogIndicatorRefresh(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { u.li.Refresh() - assert.Equal(t, u.li.GetText(false), u.e) + assert.Equal(t, u.e, u.li.GetText(false)) }) } } + +func BenchmarkLogIndicatorRefresh(b *testing.B) { + defaults := config.NewStyles() + v := view.NewLogIndicator(config.NewConfig(nil), defaults, true) + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.Refresh() + } +} diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go index e864bf2d..026a877e 100644 --- a/internal/view/log_int_test.go +++ b/internal/view/log_int_test.go @@ -23,7 +23,7 @@ func TestLogAutoScroll(t *testing.T) { v.GetModel().Set(ii) v.GetModel().Notify() - assert.Equal(t, 15, len(v.Hints())) + assert.Equal(t, 16, len(v.Hints())) v.toggleAutoScrollCmd(nil) assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true)) @@ -75,8 +75,7 @@ func TestLogTimestamp(t *testing.T) { &dao.LogItem{ Pod: "fred/blee", Container: "c1", - Timestamp: "ttt", - Bytes: []byte("Testing 1, 2, 3"), + Bytes: []byte("ttt Testing 1, 2, 3\n"), }, ) var list logList @@ -84,9 +83,11 @@ func TestLogTimestamp(t *testing.T) { l.GetModel().Set(ii) l.SendKeys(ui.KeyT) l.Logs().Clear() - l.Flush(ii.Lines(true)) + ll := make([][]byte, ii.Len()) + ii.Lines(0, true, ll) + l.Flush(ll) - assert.Equal(t, fmt.Sprintf("\n%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3"), l.Logs().GetText(true)) + assert.Equal(t, fmt.Sprintf("%-30s %s", "ttt", "fred/blee c1 Testing 1, 2, 3\n"), l.Logs().GetText(true)) assert.Equal(t, 2, list.change) assert.Equal(t, 2, list.clear) assert.Equal(t, 0, list.fail) @@ -131,5 +132,8 @@ func (l *logList) LogChanged(ll [][]byte) { l.lines += string(line) } } +func (l *logList) LogCanceled() {} +func (l *logList) LogStop() {} +func (l *logList) LogResume() {} func (l *logList) LogCleared() { l.clear++ } func (l *logList) LogFailed(error) { l.fail++ } diff --git a/internal/view/log_test.go b/internal/view/log_test.go index 501f8e92..9ee08d1c 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -25,10 +25,32 @@ func TestLog(t *testing.T) { v.Init(makeContext()) ii := dao.NewLogItems() - ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")) - v.Flush(ii.Lines(false)) + ii.Add(dao.NewLogItemFromString("blee\n"), dao.NewLogItemFromString("bozo\n")) + ll := make([][]byte, ii.Len()) + ii.Lines(0, false, ll) + v.Flush(ll) - assert.Equal(t, 29, len(v.Logs().GetText(true))) + assert.Equal(t, "Waiting for logs...\nblee\nbozo\n", v.Logs().GetText(true)) +} + +func TestLogFlush(t *testing.T) { + opts := dao.LogOptions{ + Path: "fred/p1", + Container: "blee", + } + v := view.NewLog(client.NewGVR("v1/pods"), &opts) + v.Init(makeContext()) + + items := dao.NewLogItems() + items.Add( + dao.NewLogItemFromString("\033[0;30mblee\n"), + dao.NewLogItemFromString("\033[0;32mBozo\n"), + ) + ll := make([][]byte, items.Len()) + items.Lines(0, false, ll) + v.Flush(ll) + + assert.Equal(t, "[orange::d]Waiting for logs...\n[black:]blee\n[green:]Bozo\n\n", v.Logs().GetText(false)) } func BenchmarkLogFlush(b *testing.B) { @@ -41,13 +63,17 @@ func BenchmarkLogFlush(b *testing.B) { items := dao.NewLogItems() items.Add( - dao.NewLogItemFromString("blee"), - dao.NewLogItemFromString("bozo"), + dao.NewLogItemFromString("\033[0;30mblee\n"), + dao.NewLogItemFromString("\033[0;101mBozo\n"), + dao.NewLogItemFromString("\033[0;101mBozo\n"), ) + ll := make([][]byte, items.Len()) + items.Lines(0, false, ll) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - v.Flush(items.Lines(false)) + v.Flush(ll) } } @@ -76,7 +102,10 @@ func TestLogViewSave(t *testing.T) { app := makeApp() ii := dao.NewLogItems() ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")) - v.Flush(ii.Lines(false)) + ll := make([][]byte, ii.Len()) + ii.Lines(0, false, ll) + v.Flush(ll) + config.K9sDumpDir = "/tmp" dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) c1, _ := os.ReadDir(dir) diff --git a/internal/view/logger.go b/internal/view/logger.go index c427ac7b..bb05da19 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -59,8 +59,7 @@ func (l *Logger) Init(_ context.Context) error { func (l *Logger) BufferChanged(s string) {} // BufferCompleted indicates input was accepted. -func (l *Logger) BufferCompleted(s string) { -} +func (l *Logger) BufferCompleted(s string) {} // BufferActive indicates the buff activity changed. func (l *Logger) BufferActive(state bool, k model.BufferKind) { diff --git a/internal/view/pf.go b/internal/view/pf.go index 5b7556cf..377c0f0f 100644 --- a/internal/view/pf.go +++ b/internal/view/pf.go @@ -3,6 +3,7 @@ package view import ( "context" "fmt" + "regexp" "time" "github.com/derailed/k9s/internal" @@ -138,19 +139,35 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - path := p.GetTable().GetSelectedItem() - if path == "" { - return nil + selections := p.GetTable().GetSelectedItems() + if len(selections) == 0 { + return evt } - showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", path), func() { - var pf dao.PortForward - pf.Init(p.App().factory, client.NewGVR("portforwards")) - if err := pf.Delete(path, true, true); err != nil { + p.Stop() + defer p.Start() + var msg string + if len(selections) > 1 { + msg = fmt.Sprintf("Delete %d marked %s?", len(selections), p.GVR()) + } else { + h, err := pfToHuman(selections[0]) + if err == nil { + msg = fmt.Sprintf("Delete %s %s?", p.GVR().R(), h) + } else { p.App().Flash().Err(err) - return + return nil } - p.App().Flash().Infof("PortForward %s deleted!", path) + } + showModal(p.App().Content.Pages, msg, func() { + for _, s := range selections { + var pf dao.PortForward + pf.Init(p.App().factory, client.NewGVR("portforwards")) + if err := pf.Delete(s, true, true); err != nil { + p.App().Flash().Err(err) + return + } + } + p.App().Flash().Infof("Successfully deleted %d PortForward!", len(selections)) p.GetTable().Refresh() }) @@ -160,6 +177,16 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { // ---------------------------------------------------------------------------- // Helpers... +var selRx = regexp.MustCompile(`\A([\w-]+)/([\w-]+)\|([\w-]+)\|(\d+):(\d+)`) + +func pfToHuman(s string) (string, error) { + mm := selRx.FindStringSubmatch(s) + if len(mm) < 6 { + return "", fmt.Errorf("Unable to parse selection %s", s) + } + return fmt.Sprintf("%s::%s %s->%s", mm[2], mm[3], mm[4], mm[5]), nil +} + func showModal(p *ui.Pages, msg string, ok func()) { m := tview.NewModal(). AddButtons([]string{"Cancel", "OK"}). diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index e84e0aee..89921c33 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -7,18 +7,20 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" + "github.com/rs/zerolog/log" ) const portForwardKey = "portforward" // PortForwardCB represents a port-forward callback function. -type PortForwardCB func(v ResourceViewer, path, co string, mapper []client.PortTunnel) +type PortForwardCB func(ResourceViewer, string, port.PortTunnels) error // ShowPortForwards pops a port forwarding configuration dialog. -func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string, okFn PortForwardCB) { +func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpecs, aa port.Annotations, okFn PortForwardCB) { styles := v.App().Styles.Dialog() f := tview.NewForm() @@ -32,37 +34,28 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string, address := v.App().Config.CurrentCluster().PortForwardAddress - var p1, p2 string - if len(ports) > 0 { - p1, p2 = ports[0], extractPort(ports[0]) - if len(ann) != 0 { - container, port, ok := parsePFAnn(ann) - if ok { - for _, p := range ports { - co, po, portNum := parsePort(p) - if co == container && port == po || port == portNum { - p1, p2 = p, extractPort(p) - break - } - } - } - } + pf, err := aa.PreferredPorts(ports) + if err != nil { + log.Warn().Err(err).Msgf("unable to resolve ports") } + + p1, p2 := pf.ToPortSpec(ports) fieldLen := int(math.Max(30, float64(len(p1)))) - f.AddInputField("Container Port:", p1, fieldLen, nil, func(p string) { - p1 = p - }) - field := f.GetFormItemByLabel("Container Port:").(*tview.InputField) - if field.GetText() == "" { - field.SetPlaceholder("Enter a container name/port") + f.AddInputField("Container Port:", p1, fieldLen, nil, nil) + coField := f.GetFormItemByLabel("Container Port:").(*tview.InputField) + if coField.GetText() == "" { + coField.SetPlaceholder("Enter a container name/port") } - f.AddInputField("Local Port:", p2, fieldLen, nil, func(p string) { - p2 = p - }) - field = f.GetFormItemByLabel("Local Port:").(*tview.InputField) - if field.GetText() == "" { - field.SetPlaceholder("Enter a local port") + f.AddInputField("Local Port:", p2, fieldLen, nil, nil) + poField := f.GetFormItemByLabel("Local Port:").(*tview.InputField) + if poField.GetText() == "" { + poField.SetPlaceholder("Enter a local port") } + coField.SetChangedFunc(func(s string) { + port := extractPort(s) + poField.SetText(port) + p2 = port + }) f.AddInputField("Address:", address, fieldLen, nil, func(h string) { address = h }) @@ -76,21 +69,18 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string, } f.AddButton("OK", func() { - pp1 := strings.Split(p1, ",") - pp2 := strings.Split(p2, ",") - if len(pp1) == 0 || len(pp1) != len(pp2) { + if coField.GetText() == "" || poField.GetText() == "" { v.App().Flash().Err(fmt.Errorf("container to local port mismatch")) return } - var tt []client.PortTunnel - for i := range pp1 { - tt = append(tt, client.PortTunnel{ - Address: address, - LocalPort: pp2[i], - ContainerPort: extractPort(pp1[i]), - }) + tt, err := port.ToTunnels(address, coField.GetText(), poField.GetText()) + if err != nil { + v.App().Flash().Err(err) + return + } + if err := okFn(v, path, tt); err != nil { + v.App().Flash().Err(err) } - okFn(v, path, extractContainer(pp1[0]), tt) }) pages := v.App().Content.Pages f.AddButton("Cancel", func() { @@ -108,7 +98,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string, modal := tview.NewModalForm("", f) msg := path if len(ports) > 1 { - msg += "\n\nExposed Ports:\n" + strings.Join(ports, "\n") + msg += "\n\nExposed Ports:\n" + ports.Dump() } modal.SetText(msg) modal.SetTextColor(styles.FgColor.Color()) @@ -131,16 +121,6 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) { // ---------------------------------------------------------------------------- // Helpers... -func parsePort(p string) (string, string, string) { - rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(╱UDP)?\z`) - mm := rx.FindStringSubmatch(p) - if len(mm) != 5 { - return "", "", "" - } - - return mm[1], mm[2], mm[3] -} - func extractPort(p string) string { rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(╱UDP)?\z`) mm := rx.FindStringSubmatch(p) diff --git a/internal/view/pf_dialog_test.go b/internal/view/pf_dialog_test.go index 8f8854ab..284af6b1 100644 --- a/internal/view/pf_dialog_test.go +++ b/internal/view/pf_dialog_test.go @@ -6,44 +6,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestExtractPort(t *testing.T) { - uu := map[string]struct { - port, e string - }{ - "empty": { - "", "", - }, - "full": { - "co/fred:8000", "8000", - }, - "named": { - "fred:8000", "8000", - }, - "port": { - "8000", "8000", - }, - "protocol": { - "dns:53╱UDP", "53", - }, - "unamed": { - "dns/53", "53", - }, - "pod-dashed": { - "blee-fred/:5000", "5000", - }, - "co-dashed": { - "blee/fred-doh:5000", "5000", - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, extractPort(u.port)) - }) - } -} - func TestExtractContainer(t *testing.T) { uu := map[string]struct { port, e string diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index 3740d585..c090e7b4 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -1,13 +1,10 @@ package view import ( - "errors" "fmt" - "net" - "strconv" - "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/watch" "github.com/gdamore/tcell/v2" @@ -19,8 +16,6 @@ import ( "k8s.io/client-go/tools/portforward" ) -const AnnDefaultPF = "k9s.imhotep.io/default-portforward-container" - // PortForwardExtender adds port-forward extensions. type PortForwardExtender struct { ResourceViewer @@ -78,19 +73,11 @@ func (p *PortForwardExtender) fetchPodName(path string) (string, error) { // ---------------------------------------------------------------------------- // Helpers... -func tryListenPort(address, port string) error { - server, err := net.Listen("tcp", fmt.Sprintf("%s:%s", address, port)) - if err != nil { - return err - } - return server.Close() -} - func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForwarder) { v.App().factory.AddForwarder(pf) v.App().QueueUpdateDraw(func() { - v.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) + v.App().Flash().Infof("PortForward activated %s", pf.ID()) DismissPortForwards(v, v.App().Content.Pages) }) @@ -106,67 +93,77 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward }) } -func startFwdCB(v ResourceViewer, path, co string, tt []client.PortTunnel) { - for _, t := range tt { - err := tryListenPort(t.Address, t.LocalPort) - if err != nil { - v.App().Flash().Err(err) - return +func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error { + if err := pts.CheckAvailable(); err != nil { + return err + } + + for _, pt := range pts { + if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, pt.Container, pt.PortMap())); ok { + return fmt.Errorf("A port-forward is already active on pod %s", path) } + pf := dao.NewPortForwarder(v.App().factory) + fwd, err := pf.Start(path, pt) + if err != nil { + return err + } + log.Debug().Msgf(">>> Starting port forward %q -- %#v", pf.ID(), pt) + go runForward(v, pf, fwd) } - if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, co)); ok { - v.App().Flash().Err(errors.New("A port-forward is already active on this pod")) - return - } - - pf := dao.NewPortForwarder(v.App().factory) - fwd, err := pf.Start(path, co, tt) - if err != nil { - v.App().Flash().Err(err) - return - } - - log.Debug().Msgf(">>> Starting port forward %q %#v", path, tt) - go runForward(v, pf, fwd) + return nil } func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { - mm, coPort, err := fetchPodPorts(v.App().factory, path) + mm, anns, err := fetchPodPorts(v.App().factory, path) if err != nil { return err } - ports := make([]string, 0, len(mm)) + ports := make(port.ContainerPortSpecs, 0, len(mm)) for co, pp := range mm { for _, p := range pp { if p.Protocol != v1.ProtocolTCP { continue } - ports = append(ports, client.FQN(co, p.Name)+":"+strconv.Itoa(int(p.ContainerPort))) + ports = append(ports, port.NewPortSpec(co, p.Name, p.ContainerPort)) } } - ShowPortForwards(v, path, ports, coPort, cb) + if spec, ok := anns[port.K9sAutoPortForwardsKey]; ok { + pfs, err := port.ParsePFs(spec) + if err != nil { + return err + } + + pts, err := pfs.ToTunnels(v.App().Config.CurrentCluster().PortForwardAddress, ports, port.IsPortFree) + if err != nil { + return err + } + + return startFwdCB(v, path, pts) + } + + ShowPortForwards(v, path, ports, anns, cb) return nil } -func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, string, error) { +func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, map[string]string, error) { log.Debug().Msgf("Fetching ports on pod %q", path) o, err := f.Get("v1/pods", path, true, labels.Everything()) if err != nil { - return nil, "", err + return nil, nil, err } var pod v1.Pod err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) if err != nil { - return nil, "", err + return nil, nil, err } - pp := make(map[string][]v1.ContainerPort) + pp := make(map[string][]v1.ContainerPort, len(pod.Spec.Containers)) for _, co := range pod.Spec.Containers { pp[co.Name] = co.Ports } - return pp, pod.Annotations[AnnDefaultPF], nil + return pp, pod.Annotations, nil } diff --git a/internal/view/pod.go b/internal/view/pod.go index 342e58f7..6146442a 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -176,7 +176,6 @@ func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey { p.App().Flash().Infof("Delete resource %s %s", p.GVR(), selections[0]) } p.GetTable().ShowDeleted() - log.Debug().Msgf("SELS %v", selections) for _, path := range selections { if err := nuker.Delete(path, true, true); err != nil { p.App().Flash().Errf("Delete failed with %s", err) diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index 1d89c754..bc09891b 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -36,7 +36,7 @@ func (r *RestartExtender) bindKeys(aa ui.KeyActions) { func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { paths := r.GetTable().GetSelectedItems() - if len(paths) == 0 { + if len(paths) == 0 || paths[0] == "" { return nil } diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index 49816a67..d604ab78 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -36,26 +36,30 @@ func (s *ScaleExtender) bindKeys(aa ui.KeyActions) { } func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { - path := s.GetTable().GetSelectedItem() - if path == "" { + paths := s.GetTable().GetSelectedItems() + if len(paths) == 0 { return nil } s.Stop() defer s.Start() - s.showScaleDialog(path) + s.showScaleDialog(paths) return nil } -func (s *ScaleExtender) showScaleDialog(path string) { - form, err := s.makeScaleForm(path) +func (s *ScaleExtender) showScaleDialog(paths []string) { + form, err := s.makeScaleForm(paths) if err != nil { s.App().Flash().Err(err) return } confirm := tview.NewModalForm("", form) - confirm.SetText(fmt.Sprintf("Scale %s %s", s.GVR(), path)) + msg := fmt.Sprintf("Scale %s %s?", s.GVR().R(), paths[0]) + if len(paths) > 1 { + msg = fmt.Sprintf("Scale [%d] %s?", len(paths), s.GVR().R()) + } + confirm.SetText(msg) confirm.SetDoneFunc(func(int, string) { s.dismissDialog() }) @@ -71,40 +75,49 @@ func (s *ScaleExtender) valueOf(col string) (string, error) { return s.GetTable().GetSelectedCell(colIdx), nil } -func (s *ScaleExtender) makeScaleForm(sel string) (*tview.Form, error) { +func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) { f := s.makeStyledForm() - replicas, err := s.valueOf("READY") - if err != nil { - return nil, err + factor := "0" + if len(sels) == 1 { + replicas, err := s.valueOf("READY") + if err != nil { + return nil, err + } + tokens := strings.Split(replicas, "/") + if len(tokens) < 2 { + return nil, fmt.Errorf("unable to locate replicas from %s", replicas) + } + factor = strings.TrimRight(tokens[1], ui.DeltaSign) } - tokens := strings.Split(replicas, "/") - if len(tokens) < 2 { - return nil, fmt.Errorf("unable to locate replicas from %s", replicas) - } - replicas = strings.TrimRight(tokens[1], ui.DeltaSign) - f.AddInputField("Replicas:", replicas, 4, func(textToCheck string, lastChar rune) bool { + f.AddInputField("Replicas:", factor, 4, func(textToCheck string, lastChar rune) bool { _, err := strconv.Atoi(textToCheck) return err == nil }, func(changed string) { - replicas = changed + factor = changed }) f.AddButton("OK", func() { defer s.dismissDialog() - count, err := strconv.Atoi(replicas) + count, err := strconv.Atoi(factor) if err != nil { s.App().Flash().Err(err) return } ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout()) defer cancel() - if err := s.scale(ctx, sel, count); err != nil { - log.Error().Err(err).Msgf("DP %s scaling failed", sel) - s.App().Flash().Err(err) - return + for _, sel := range sels { + if err := s.scale(ctx, sel, count); err != nil { + log.Error().Err(err).Msgf("DP %s scaling failed", sel) + s.App().Flash().Err(err) + return + } + } + if len(sels) == 1 { + s.App().Flash().Infof("[%d] %s scaled successfully", len(sels), s.GVR().R()) + } else { + s.App().Flash().Infof("%s %s scaled successfully", s.GVR().R(), sels[0]) } - s.App().Flash().Infof("Resource %s:%s scaled successfully", s.GVR(), sel) }) f.AddButton("Cancel", func() { diff --git a/internal/view/table.go b/internal/view/table.go index d941a07f..b6d2b571 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -2,6 +2,7 @@ package view import ( "context" + "strings" "time" "github.com/atotto/clipboard" @@ -53,9 +54,17 @@ func (t *Table) Init(ctx context.Context) (err error) { } // HeaderIndex returns index of a given column or false if not found. -func (t *Table) HeaderIndex(header string) (int, bool) { +func (t *Table) HeaderIndex(colName string) (int, bool) { for i := 0; i < t.GetColumnCount(); i++ { - if h := t.GetCell(0, i); h != nil && h.Text == header { + h := t.GetCell(0, i) + if h == nil { + continue + } + s := h.Text + if idx := strings.Index(s, "["); idx > 0 { + s = s[:idx] + } + if s == colName { return i, true } } diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 300081aa..a5b3d5ef 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -244,11 +244,8 @@ func (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, err func (f *Factory) AddForwarder(pf Forwarder) { f.mx.Lock() defer f.mx.Unlock() - f.forwarders[pf.Path()] = pf - for k, v := range f.forwarders { - log.Debug().Msgf("%q -- %#v", k, v) - } + f.forwarders[pf.Path()] = pf } // DeleteForwarder deletes portforward for a given container. @@ -277,11 +274,20 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) { return fwd, ok } +// BOZO!! Review!!! // ValidatePortForwards check if pods are still around for portforwards. func (f *Factory) ValidatePortForwards() { for k, fwd := range f.forwarders { tokens := strings.Split(k, ":") - _, err := f.Get("v1/pods", tokens[0], false, labels.Everything()) + if len(tokens) != 2 { + log.Error().Msgf("Invalid fwd keys %q", k) + return + } + paths := strings.Split(tokens[0], "|") + if len(paths) < 1 { + log.Error().Msgf("Invalid path %q", tokens[0]) + } + _, err := f.Get("v1/pods", paths[0], false, labels.Everything()) if err != nil { fwd.Stop() delete(f.forwarders, k) diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go index d30bb053..c0a0c28b 100644 --- a/internal/watch/forwarders.go +++ b/internal/watch/forwarders.go @@ -3,7 +3,7 @@ package watch import ( "strings" - "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/port" "github.com/rs/zerolog/log" "k8s.io/client-go/tools/portforward" ) @@ -11,19 +11,22 @@ import ( // Forwarder represents a port forwarder. type Forwarder interface { // Start starts a port-forward. - Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) + Start(path string, tunnel port.PortTunnel) (*portforward.PortForwarder, error) // Stop terminates a port forward. Stop() + // ID returns the pf id. + ID() string + // Path returns a resource FQN. Path() string // Container returns a container name. Container() string - // Ports returns container exposed ports. - Ports() []string + // Ports returns the port mapping. + Port() string // FQN returns the full port-forward name. FQN() string @@ -49,10 +52,11 @@ func NewForwarders() Forwarders { return make(map[string]Forwarder) } +// BOZO!! Review!!! // IsPodForwarded checks if pod has a forward. func (ff Forwarders) IsPodForwarded(path string) bool { for k := range ff { - fqn := strings.Split(k, ":") + fqn := strings.Split(k, "|") if fqn[0] == path { return true } @@ -78,18 +82,14 @@ func (ff Forwarders) DeleteAll() { // Kill stops and delete a port-forwards associated with pod. func (ff Forwarders) Kill(path string) int { - hasContainer := strings.Contains(path, ":") var stats int for k, f := range ff { victim := k - if !hasContainer { - victim = strings.Split(k, ":")[0] - } if victim == path { stats++ - log.Debug().Msgf("Stop + Delete port-forward %s", k) + log.Debug().Msgf("Stop + Delete port-forward %s", victim) f.Stop() - delete(ff, k) + delete(ff, victim) } }