diff --git a/Makefile b/Makefile index d0402477..fa2db228 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.26.5 +VERSION ?= v0.26.6 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/change_logs/release_v0.26.6.md b/change_logs/release_v0.26.6.md new file mode 100644 index 00000000..4a80ff68 --- /dev/null +++ b/change_logs/release_v0.26.6.md @@ -0,0 +1,32 @@ + + +# Release v0.26.6 + +## 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! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!! + +As you may know, K9s is not pimped out by corps with deep pockets, thus 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) + +--- + +## Maintenance Release + +--- + +## Resolved Issues + +* [Issue #1773](https://github.com/derailed/k9s/issues/1773) CustomResourceDefinition does not display + +## Contributed PRs (Thank you!!) + +* [PR #1777](https://github.com/derailed/k9s/pull/1777) Fix directory path when viewing screendump +* [PR #1776](https://github.com/derailed/k9s/pull/1776) Add a closing tag when showing timestamp in log view +* [PR #1775](https://github.com/derailed/k9s/pull/1775) Log toggles: add a space after "on" in logs view +* [PR #1772](https://github.com/derailed/k9s/pull/1772) docs: update homebrew installation note + +--- + + © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 77f6fe7d..58fbdcee 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -4,6 +4,7 @@ import ( "os" "os/user" "path/filepath" + "regexp" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" @@ -16,6 +17,13 @@ const ( DefaultFileMod os.FileMode = 0600 ) +var invalidPathCharsRX = regexp.MustCompile(`[:]+`) + +// SanitizeFilename sanitizes the dump filename. +func SanitizeFilename(name string) string { + return invalidPathCharsRX.ReplaceAllString(name, "-") +} + // InList check if string is in a collection of strings. func InList(ll []string, n string) bool { for _, l := range ll { diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 2ed38fff..964fcbf1 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -47,6 +47,10 @@ func NewK9s() *K9s { } } +func (k *K9s) CurrentContextDir() string { + return SanitizeFilename(k.CurrentContext) +} + // ActivateCluster initializes the active cluster is not present. func (k *K9s) ActivateCluster(ns string) { if _, ok := k.Clusters[k.CurrentCluster]; ok { diff --git a/internal/dao/crd.go b/internal/dao/crd.go index 516ca87b..d05d5f9a 100644 --- a/internal/dao/crd.go +++ b/internal/dao/crd.go @@ -4,6 +4,7 @@ import ( "context" "github.com/derailed/k9s/internal" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -18,6 +19,19 @@ type CustomResourceDefinition struct { Resource } +// IsHappy check for happy deployments. +func (c *CustomResourceDefinition) IsHappy(crd v1.CustomResourceDefinition) bool { + versions := make([]string, 0, 3) + for _, v := range crd.Spec.Versions { + if v.Served && !v.Deprecated { + versions = append(versions, v.Name) + break + } + } + + return len(versions) > 0 +} + // List returns a collection of nodes. func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtime.Object, error) { strLabel, ok := ctx.Value(internal.KeyLabels).(string) diff --git a/internal/dao/screen_dump.go b/internal/dao/screen_dump.go index 1fc00caf..c1c60925 100644 --- a/internal/dao/screen_dump.go +++ b/internal/dao/screen_dump.go @@ -4,7 +4,6 @@ import ( "context" "errors" "os" - "regexp" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/render" @@ -15,9 +14,6 @@ import ( var ( _ Accessor = (*ScreenDump)(nil) _ Nuker = (*ScreenDump)(nil) - - // InvalidCharsRX contains invalid filename characters. - invalidPathCharsRX = regexp.MustCompile(`[:]+`) ) // ScreenDump represents a scraped resources. @@ -41,7 +37,6 @@ func (d *ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, erro if err != nil { return nil, err } - oo := make([]runtime.Object, len(ff)) for i, f := range ff { if fi, err := f.Info(); err == nil { @@ -51,10 +46,3 @@ func (d *ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, erro return oo, nil } - -// Helpers... - -// SanitizeFilename sanitizes the dump filename. -func SanitizeFilename(name string) string { - return invalidPathCharsRX.ReplaceAllString(name, "-") -} diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go index 76694a90..8cd9b10e 100644 --- a/internal/model/pulse_health.go +++ b/internal/model/pulse_health.go @@ -122,7 +122,7 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, } rows := make(render.Rows, len(table.Rows)) re, _ := meta.Renderer.(Generic) - re.SetTable(table) + re.SetTable(ns, table) for i, row := range table.Rows { if err := re.Render(row, ns, &rows[i]); err != nil { return nil, err diff --git a/internal/model/table.go b/internal/model/table.go index d16e2e31..799c385d 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -306,10 +306,16 @@ func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error return nil } +// Generic represents a generic resource. type Generic interface { - SetTable(*metav1beta1.Table) - Header(string) render.Header - Render(interface{}, string, *render.Row) error + // SetTable sets up the resource tabular definition. + SetTable(ns string, table *metav1beta1.Table) + + // Header returns a resource header. + Header(ns string) render.Header + + // Render renders the resource. + Render(o interface{}, ns string, row *render.Row) error } func genericHydrate(ns string, table *metav1beta1.Table, rr render.Rows, re Renderer) error { @@ -317,7 +323,7 @@ func genericHydrate(ns string, table *metav1beta1.Table, rr render.Rows, re Rend if !ok { return fmt.Errorf("expecting generic renderer but got %T", re) } - gr.SetTable(table) + gr.SetTable(ns, table) for i, row := range table.Rows { if err := gr.Render(row, ns, &rr[i]); err != nil { return err diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index 8c6c5891..7667efb8 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -128,7 +128,7 @@ func TestTableGenericHydrate(t *testing.T) { } rr := make([]render.Row, 2) re := render.Generic{} - re.SetTable(&tt) + re.SetTable("blee", &tt) assert.Nil(t, genericHydrate("blee", &tt, rr, &re)) assert.Equal(t, 2, len(rr)) diff --git a/internal/model/tree.go b/internal/model/tree.go index 272c9e54..3287d406 100644 --- a/internal/model/tree.go +++ b/internal/model/tree.go @@ -305,7 +305,7 @@ func genericTreeHydrate(ctx context.Context, ns string, table *metav1beta1.Table return fmt.Errorf("expecting xray.Generic renderer but got %T", re) } - tre.SetTable(table) + tre.SetTable(ns, table) // BOZO!! Need table row sorter!! for _, row := range table.Rows { if err := tre.Render(ctx, ns, row); err != nil { diff --git a/internal/render/crd.go b/internal/render/crd.go index fcce6cec..0e6c2b00 100644 --- a/internal/render/crd.go +++ b/internal/render/crd.go @@ -1,7 +1,9 @@ package render import ( + "errors" "fmt" + "strings" "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" @@ -19,13 +21,15 @@ type CustomResourceDefinition struct { func (CustomResourceDefinition) Header(string) Header { return Header{ HeaderColumn{Name: "NAME"}, + HeaderColumn{Name: "VERSIONS"}, HeaderColumn{Name: "LABELS", Wide: true}, + HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { +func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("Expected CustomResourceDefinition, but got %T", o) @@ -37,27 +41,68 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { return err } - var version string + versions := make([]string, 0, 3) for _, v := range crd.Spec.Versions { - if v.Served && !v.Deprecated { - version = v.Name - break + if v.Served { + n := v.Name + if v.Deprecated { + n += "!" + } + versions = append(versions, n) } } - if version == "" { - return fmt.Errorf("unable to assert resource version") + if len(versions) == 0 { + log.Warn().Msgf("unable to assert CRD versions for %s", crd.GetName()) } r.ID = client.FQN(client.ClusterScope, crd.GetName()) r.Fields = Fields{ crd.GetName(), + naStrings(versions), mapToIfc(crd.GetLabels()), + asStatus(c.diagnose(crd.GetName(), crd.Spec.Versions)), toAge(crd.GetCreationTimestamp()), } return nil } +func (c CustomResourceDefinition) diagnose(n string, vv []v1.CustomResourceDefinitionVersion) error { + if len(vv) == 0 { + return fmt.Errorf("unable to assert CRD servers versions for %s", n) + } + + var ( + ee []error + served bool + ) + for _, v := range vv { + if v.Served { + served = true + } + if v.Deprecated { + if v.DeprecationWarning != nil { + ee = append(ee, fmt.Errorf("%s", *v.DeprecationWarning)) + } else { + ee = append(ee, fmt.Errorf("%s[%s] is deprecated!", n, v.Name)) + } + } + } + if !served { + ee = append(ee, fmt.Errorf("CRD %s is no longer served by the api server", n)) + } + + if len(ee) == 0 { + return nil + } + errs := make([]string, 0, len(ee)) + for _, e := range ee { + errs = append(errs, e.Error()) + } + + return errors.New(strings.Join(errs, " - ")) +} + func extractMetaField(m map[string]interface{}, field string) string { f, ok := m[field] if !ok { diff --git a/internal/render/generic.go b/internal/render/generic.go index 18b5fc03..d049d204 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/rs/zerolog/log" "strings" "github.com/derailed/k9s/internal/client" @@ -16,6 +17,7 @@ const ageTableCol = "Age" type Generic struct { Base table *metav1beta1.Table + header Header ageIndex int } @@ -24,8 +26,9 @@ func (*Generic) IsGeneric() bool { } // SetTable sets the tabular resource. -func (g *Generic) SetTable(t *metav1beta1.Table) { +func (g *Generic) SetTable(ns string, t *metav1beta1.Table) { g.table = t + g.header = g.Header(ns) } // ColorerFunc colors a resource row. @@ -35,6 +38,9 @@ func (*Generic) ColorerFunc() ColorerFunc { // Header returns a header row. func (g *Generic) Header(ns string) Header { + if g.header != nil { + return g.header + } if g.table == nil { return Header{} } @@ -89,6 +95,8 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { } if d, ok := duration.(string); ok { r.Fields = append(r.Fields, d) + } else { + log.Warn().Msgf("No Duration detected on age field") } return nil diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index 71bf1e6e..7f3f4884 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -83,7 +83,7 @@ func TestGenericRender(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { var r render.Row - re.SetTable(u.table) + re.SetTable(u.ns, u.table) assert.Equal(t, u.eHeader, re.Header(u.ns)) assert.Nil(t, re.Render(u.table.Rows[0], u.ns, &r)) diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 0309bfed..2e5413f7 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -72,6 +72,7 @@ func Happy(ns string, h Header, r Row) bool { if validCol < 0 { return true } + return strings.TrimSpace(r.Fields[validCol]) == "" } @@ -173,6 +174,13 @@ func missing(s string) string { return check(s, MissingValue) } +func naStrings(ss []string) string { + if len(ss) == 0 { + return NAValue + } + return strings.Join(ss, ",") +} + func na(s string) string { return check(s, NAValue) } diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index d503a6f8..0aaf1134 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -65,7 +65,7 @@ func fileToSubject(path string) string { } func benchDir(cfg *config.Config) string { - return filepath.Join(perf.K9sBenchDir, cfg.K9s.CurrentCluster) + return filepath.Join(perf.K9sBenchDir, cfg.K9s.CurrentContextDir()) } func readBenchFile(cfg *config.Config, n string) (string, error) { diff --git a/internal/view/details.go b/internal/view/details.go index fec1cc82..6db69033 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -282,7 +282,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(d.app.Config.K9s.GetScreenDumpDir(), d.app.Config.K9s.CurrentCluster, d.title, d.text.GetText(true)); err != nil { + if path, err := saveYAML(d.app.Config.K9s.GetScreenDumpDir(), d.app.Config.K9s.CurrentContextDir(), d.title, d.text.GetText(true)); err != nil { d.app.Flash().Err(err) } else { d.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/live_view.go b/internal/view/live_view.go index ac81e39c..9528e736 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -331,7 +331,7 @@ func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *LiveView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.CurrentCluster, v.title, v.text.GetText(true)); err != nil { + if path, err := saveYAML(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.CurrentContextDir(), v.title, v.text.GetText(true)); err != nil { v.app.Flash().Err(err) } else { v.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/log.go b/internal/view/log.go index 54062327..7565e1cf 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -395,7 +395,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { // SaveCmd dumps the logs to file. func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { - path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true)) + path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContextDir(), l.model.GetPath(), l.logs.GetText(true)) if err != nil { l.app.Flash().Err(err) return nil @@ -409,8 +409,8 @@ func ensureDir(dir string) error { return os.MkdirAll(dir, 0744) } -func saveData(screenDumpDir, cluster, fqn, data string) (string, error) { - dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) +func saveData(screenDumpDir, context, fqn, data string) (string, error) { + dir := filepath.Join(screenDumpDir, context) if err := ensureDir(dir); err != nil { return "", err } diff --git a/internal/view/logger.go b/internal/view/logger.go index 4d1dea4f..8e950f60 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -151,7 +151,7 @@ func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentCluster, l.title, l.GetText(true)); err != nil { + if path, err := saveYAML(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContextDir(), l.title, l.GetText(true)); err != nil { l.app.Flash().Err(err) } else { l.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/screen_dump.go b/internal/view/screen_dump.go index 43eee754..cedd759b 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -8,7 +8,6 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell/v2" "github.com/rs/zerolog/log" @@ -35,7 +34,7 @@ func NewScreenDump(gvr client.GVR) ResourceViewer { } func (s *ScreenDump) dirContext(ctx context.Context) context.Context { - dir := filepath.Join(s.App().Config.K9s.GetScreenDumpDir(), dao.SanitizeFilename(s.App().Config.K9s.CurrentContext)) + dir := filepath.Join(s.App().Config.K9s.GetScreenDumpDir(), s.App().Config.K9s.CurrentContextDir()) log.Debug().Msgf("SD-DIR %q", dir) config.EnsureFullPath(dir, config.DefaultDirMod) diff --git a/internal/view/table.go b/internal/view/table.go index 989a3f9a..121f5f64 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -167,7 +167,7 @@ func (t *Table) BufferActive(state bool, k model.BufferKind) { } func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveTable(t.app.Config.K9s.GetScreenDumpDir(), t.app.Config.K9s.CurrentContext, t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { + if path, err := saveTable(t.app.Config.K9s.GetScreenDumpDir(), t.app.Config.K9s.CurrentContextDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { t.app.Flash().Err(err) } else { t.app.Flash().Infof("File %s saved successfully!", path) diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index a09075b1..e72e0cfd 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -9,21 +9,21 @@ import ( "time" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/rs/zerolog/log" ) -func computeFilename(screenDumpDir, cluster, ns, title, path string) (string, error) { +func computeFilename(screenDumpDir, context, ns, title, path string) (string, error) { now := time.Now().UnixNano() - dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) + dir := filepath.Join(screenDumpDir, context) if err := ensureDir(dir); err != nil { return "", err } - name := title + "-" + dao.SanitizeFilename(path) + name := title + "-" + config.SanitizeFilename(path) if path == "" { name = title } @@ -38,13 +38,13 @@ func computeFilename(screenDumpDir, cluster, ns, title, path string) (string, er return strings.ToLower(filepath.Join(dir, fName)), nil } -func saveTable(screenDumpDir, cluster, title, path string, data *render.TableData) (string, error) { +func saveTable(screenDumpDir, context, title, path string, data *render.TableData) (string, error) { ns := data.Namespace if client.IsClusterWide(ns) { ns = client.NamespaceAll } - fPath, err := computeFilename(screenDumpDir, cluster, ns, title, path) + fPath, err := computeFilename(screenDumpDir, context, ns, title, path) if err != nil { return "", err } diff --git a/internal/view/yaml.go b/internal/view/yaml.go index 4ed23441..d95d7dc7 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -9,7 +9,6 @@ import ( "time" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/tview" "github.com/rs/zerolog/log" ) @@ -61,14 +60,14 @@ func enableRegion(str string) string { return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]") } -func saveYAML(screenDumpDir, cluster, name, data string) (string, error) { - dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) +func saveYAML(screenDumpDir, context, name, data string) (string, error) { + dir := filepath.Join(screenDumpDir, context) if err := ensureDir(dir); err != nil { return "", err } now := time.Now().UnixNano() - fName := fmt.Sprintf("%s-%d.yml", dao.SanitizeFilename(name), now) + fName := fmt.Sprintf("%s-%d.yml", config.SanitizeFilename(name), now) path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_WRONLY diff --git a/internal/xray/generic.go b/internal/xray/generic.go index 8287ce7d..af733019 100644 --- a/internal/xray/generic.go +++ b/internal/xray/generic.go @@ -14,7 +14,7 @@ type Generic struct { } // SetTable sets the tabular resource. -func (g *Generic) SetTable(t *metav1beta1.Table) { +func (g *Generic) SetTable(_ string, t *metav1beta1.Table) { g.table = t }