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/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 {

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.
func (k *K9s) ActivateCluster(ns string) {
if _, ok := k.Clusters[k.CurrentCluster]; ok {

View File

@ -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)

View File

@ -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, "-")
}

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))
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

View File

@ -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

View File

@ -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))

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)
}
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 {

View File

@ -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 {

View File

@ -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

View File

@ -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))

View File

@ -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)
}

View File

@ -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) {

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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
}