fix #1773
parent
c35949189f
commit
39a55231fa
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, "-")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue