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 6a9ab91e..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. @@ -37,11 +33,10 @@ func (d *ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, erro return nil, errors.New("no screendump dir found in context") } - ff, err := os.ReadDir(SanitizeFilename(dir)) + ff, err := os.ReadDir(dir) 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 5a975ae7..cedd759b 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -34,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(), s.App().Config.K9s.CurrentCluster) + 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 8cfd90cc..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.CurrentCluster, 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 }