derailed 2022-09-26 19:27:43 -06:00
parent c35949189f
commit 39a55231fa
22 changed files with 129 additions and 49 deletions

View File

@ -4,6 +4,7 @@ import (
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"regexp"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@ -16,6 +17,13 @@ const (
DefaultFileMod os.FileMode = 0600 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. // InList check if string is in a collection of strings.
func InList(ll []string, n string) bool { func InList(ll []string, n string) bool {
for _, l := range ll { for _, l := range ll {

View File

@ -47,6 +47,10 @@ func NewK9s() *K9s {
} }
} }
func (k *K9s) CurrentContextDir() string {
return SanitizeFilename(k.CurrentContext)
}
// ActivateCluster initializes the active cluster is not present. // ActivateCluster initializes the active cluster is not present.
func (k *K9s) ActivateCluster(ns string) { func (k *K9s) ActivateCluster(ns string) {
if _, ok := k.Clusters[k.CurrentCluster]; ok { if _, ok := k.Clusters[k.CurrentCluster]; ok {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@ -18,6 +19,19 @@ type CustomResourceDefinition struct {
Resource 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. // List returns a collection of nodes.
func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtime.Object, error) { func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtime.Object, error) {
strLabel, ok := ctx.Value(internal.KeyLabels).(string) strLabel, ok := ctx.Value(internal.KeyLabels).(string)

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"os" "os"
"regexp"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
@ -15,9 +14,6 @@ import (
var ( var (
_ Accessor = (*ScreenDump)(nil) _ Accessor = (*ScreenDump)(nil)
_ Nuker = (*ScreenDump)(nil) _ Nuker = (*ScreenDump)(nil)
// InvalidCharsRX contains invalid filename characters.
invalidPathCharsRX = regexp.MustCompile(`[:]+`)
) )
// ScreenDump represents a scraped resources. // 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") return nil, errors.New("no screendump dir found in context")
} }
ff, err := os.ReadDir(SanitizeFilename(dir)) ff, err := os.ReadDir(dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
oo := make([]runtime.Object, len(ff)) oo := make([]runtime.Object, len(ff))
for i, f := range ff { for i, f := range ff {
if fi, err := f.Info(); err == nil { 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 return oo, nil
} }
// Helpers...
// SanitizeFilename sanitizes the dump filename.
func SanitizeFilename(name string) string {
return invalidPathCharsRX.ReplaceAllString(name, "-")
}

View File

@ -122,7 +122,7 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
} }
rows := make(render.Rows, len(table.Rows)) rows := make(render.Rows, len(table.Rows))
re, _ := meta.Renderer.(Generic) re, _ := meta.Renderer.(Generic)
re.SetTable(table) re.SetTable(ns, table)
for i, row := range table.Rows { for i, row := range table.Rows {
if err := re.Render(row, ns, &rows[i]); err != nil { if err := re.Render(row, ns, &rows[i]); err != nil {
return nil, err return nil, err

View File

@ -306,10 +306,16 @@ func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error
return nil return nil
} }
// Generic represents a generic resource.
type Generic interface { type Generic interface {
SetTable(*metav1beta1.Table) // SetTable sets up the resource tabular definition.
Header(string) render.Header SetTable(ns string, table *metav1beta1.Table)
Render(interface{}, string, *render.Row) error
// 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 { 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 { if !ok {
return fmt.Errorf("expecting generic renderer but got %T", re) return fmt.Errorf("expecting generic renderer but got %T", re)
} }
gr.SetTable(table) gr.SetTable(ns, table)
for i, row := range table.Rows { for i, row := range table.Rows {
if err := gr.Render(row, ns, &rr[i]); err != nil { if err := gr.Render(row, ns, &rr[i]); err != nil {
return err return err

View File

@ -128,7 +128,7 @@ func TestTableGenericHydrate(t *testing.T) {
} }
rr := make([]render.Row, 2) rr := make([]render.Row, 2)
re := render.Generic{} re := render.Generic{}
re.SetTable(&tt) re.SetTable("blee", &tt)
assert.Nil(t, genericHydrate("blee", &tt, rr, &re)) assert.Nil(t, genericHydrate("blee", &tt, rr, &re))
assert.Equal(t, 2, len(rr)) assert.Equal(t, 2, len(rr))

View File

@ -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) return fmt.Errorf("expecting xray.Generic renderer but got %T", re)
} }
tre.SetTable(table) tre.SetTable(ns, table)
// BOZO!! Need table row sorter!! // BOZO!! Need table row sorter!!
for _, row := range table.Rows { for _, row := range table.Rows {
if err := tre.Render(ctx, ns, row); err != nil { if err := tre.Render(ctx, ns, row); err != nil {

View File

@ -1,7 +1,9 @@
package render package render
import ( import (
"errors"
"fmt" "fmt"
"strings"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -19,13 +21,15 @@ type CustomResourceDefinition struct {
func (CustomResourceDefinition) Header(string) Header { func (CustomResourceDefinition) Header(string) Header {
return Header{ return Header{
HeaderColumn{Name: "NAME"}, HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "VERSIONS"},
HeaderColumn{Name: "LABELS", Wide: true}, HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true}, HeaderColumn{Name: "AGE", Time: true},
} }
} }
// Render renders a K8s resource to screen. // 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) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("Expected CustomResourceDefinition, but got %T", o) 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 return err
} }
var version string versions := make([]string, 0, 3)
for _, v := range crd.Spec.Versions { for _, v := range crd.Spec.Versions {
if v.Served && !v.Deprecated { if v.Served {
version = v.Name n := v.Name
break if v.Deprecated {
n += "!"
}
versions = append(versions, n)
} }
} }
if version == "" { if len(versions) == 0 {
return fmt.Errorf("unable to assert resource version") log.Warn().Msgf("unable to assert CRD versions for %s", crd.GetName())
} }
r.ID = client.FQN(client.ClusterScope, crd.GetName()) r.ID = client.FQN(client.ClusterScope, crd.GetName())
r.Fields = Fields{ r.Fields = Fields{
crd.GetName(), crd.GetName(),
naStrings(versions),
mapToIfc(crd.GetLabels()), mapToIfc(crd.GetLabels()),
asStatus(c.diagnose(crd.GetName(), crd.Spec.Versions)),
toAge(crd.GetCreationTimestamp()), toAge(crd.GetCreationTimestamp()),
} }
return nil 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 { func extractMetaField(m map[string]interface{}, field string) string {
f, ok := m[field] f, ok := m[field]
if !ok { if !ok {

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/rs/zerolog/log"
"strings" "strings"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -16,6 +17,7 @@ const ageTableCol = "Age"
type Generic struct { type Generic struct {
Base Base
table *metav1beta1.Table table *metav1beta1.Table
header Header
ageIndex int ageIndex int
} }
@ -24,8 +26,9 @@ func (*Generic) IsGeneric() bool {
} }
// SetTable sets the tabular resource. // 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.table = t
g.header = g.Header(ns)
} }
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
@ -35,6 +38,9 @@ func (*Generic) ColorerFunc() ColorerFunc {
// Header returns a header row. // Header returns a header row.
func (g *Generic) Header(ns string) Header { func (g *Generic) Header(ns string) Header {
if g.header != nil {
return g.header
}
if g.table == nil { if g.table == nil {
return Header{} return Header{}
} }
@ -89,6 +95,8 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
} }
if d, ok := duration.(string); ok { if d, ok := duration.(string); ok {
r.Fields = append(r.Fields, d) r.Fields = append(r.Fields, d)
} else {
log.Warn().Msgf("No Duration detected on age field")
} }
return nil return nil

View File

@ -83,7 +83,7 @@ func TestGenericRender(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
var r render.Row var r render.Row
re.SetTable(u.table) re.SetTable(u.ns, u.table)
assert.Equal(t, u.eHeader, re.Header(u.ns)) assert.Equal(t, u.eHeader, re.Header(u.ns))
assert.Nil(t, re.Render(u.table.Rows[0], u.ns, &r)) assert.Nil(t, re.Render(u.table.Rows[0], u.ns, &r))

View File

@ -72,6 +72,7 @@ func Happy(ns string, h Header, r Row) bool {
if validCol < 0 { if validCol < 0 {
return true return true
} }
return strings.TrimSpace(r.Fields[validCol]) == "" return strings.TrimSpace(r.Fields[validCol]) == ""
} }
@ -173,6 +174,13 @@ func missing(s string) string {
return check(s, MissingValue) return check(s, MissingValue)
} }
func naStrings(ss []string) string {
if len(ss) == 0 {
return NAValue
}
return strings.Join(ss, ",")
}
func na(s string) string { func na(s string) string {
return check(s, NAValue) return check(s, NAValue)
} }

View File

@ -65,7 +65,7 @@ func fileToSubject(path string) string {
} }
func benchDir(cfg *config.Config) 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) { func readBenchFile(cfg *config.Config, n string) (string, error) {

View File

@ -282,7 +282,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (d *Details) saveCmd(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) d.app.Flash().Err(err)
} else { } else {
d.app.Flash().Infof("Log %s saved successfully!", path) d.app.Flash().Infof("Log %s saved successfully!", path)

View File

@ -331,7 +331,7 @@ func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (v *LiveView) saveCmd(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) v.app.Flash().Err(err)
} else { } else {
v.app.Flash().Infof("Log %s saved successfully!", path) v.app.Flash().Infof("Log %s saved successfully!", path)

View File

@ -395,7 +395,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
// SaveCmd dumps the logs to file. // SaveCmd dumps the logs to file.
func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { 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 { if err != nil {
l.app.Flash().Err(err) l.app.Flash().Err(err)
return nil return nil
@ -409,8 +409,8 @@ func ensureDir(dir string) error {
return os.MkdirAll(dir, 0744) return os.MkdirAll(dir, 0744)
} }
func saveData(screenDumpDir, cluster, fqn, data string) (string, error) { func saveData(screenDumpDir, context, fqn, data string) (string, error) {
dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) dir := filepath.Join(screenDumpDir, context)
if err := ensureDir(dir); err != nil { if err := ensureDir(dir); err != nil {
return "", err return "", err
} }

View File

@ -151,7 +151,7 @@ func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (l *Logger) saveCmd(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) l.app.Flash().Err(err)
} else { } else {
l.app.Flash().Infof("Log %s saved successfully!", path) l.app.Flash().Infof("Log %s saved successfully!", path)

View File

@ -34,7 +34,7 @@ func NewScreenDump(gvr client.GVR) ResourceViewer {
} }
func (s *ScreenDump) dirContext(ctx context.Context) context.Context { 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) log.Debug().Msgf("SD-DIR %q", dir)
config.EnsureFullPath(dir, config.DefaultDirMod) config.EnsureFullPath(dir, config.DefaultDirMod)

View File

@ -167,7 +167,7 @@ func (t *Table) BufferActive(state bool, k model.BufferKind) {
} }
func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { 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) t.app.Flash().Err(err)
} else { } else {
t.app.Flash().Infof("File %s saved successfully!", path) t.app.Flash().Infof("File %s saved successfully!", path)

View File

@ -9,21 +9,21 @@ import (
"time" "time"
"github.com/derailed/k9s/internal/client" "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/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/rs/zerolog/log" "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() now := time.Now().UnixNano()
dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) dir := filepath.Join(screenDumpDir, context)
if err := ensureDir(dir); err != nil { if err := ensureDir(dir); err != nil {
return "", err return "", err
} }
name := title + "-" + dao.SanitizeFilename(path) name := title + "-" + config.SanitizeFilename(path)
if path == "" { if path == "" {
name = title name = title
} }
@ -38,13 +38,13 @@ func computeFilename(screenDumpDir, cluster, ns, title, path string) (string, er
return strings.ToLower(filepath.Join(dir, fName)), nil 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 ns := data.Namespace
if client.IsClusterWide(ns) { if client.IsClusterWide(ns) {
ns = client.NamespaceAll ns = client.NamespaceAll
} }
fPath, err := computeFilename(screenDumpDir, cluster, ns, title, path) fPath, err := computeFilename(screenDumpDir, context, ns, title, path)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -9,7 +9,6 @@ import (
"time" "time"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -61,14 +60,14 @@ func enableRegion(str string) string {
return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]") return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]")
} }
func saveYAML(screenDumpDir, cluster, name, data string) (string, error) { func saveYAML(screenDumpDir, context, name, data string) (string, error) {
dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) dir := filepath.Join(screenDumpDir, context)
if err := ensureDir(dir); err != nil { if err := ensureDir(dir); err != nil {
return "", err return "", err
} }
now := time.Now().UnixNano() 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) path := filepath.Join(dir, fName)
mod := os.O_CREATE | os.O_WRONLY mod := os.O_CREATE | os.O_WRONLY

View File

@ -14,7 +14,7 @@ type Generic struct {
} }
// SetTable sets the tabular resource. // SetTable sets the tabular resource.
func (g *Generic) SetTable(t *metav1beta1.Table) { func (g *Generic) SetTable(_ string, t *metav1beta1.Table) {
g.table = t g.table = t
} }