remove xray icons. fix #498, #497

mine
derailed 2020-01-20 10:16:00 -07:00
parent 0af1c7e0d2
commit 4d37d2449c
25 changed files with 465 additions and 165 deletions

View File

@ -64,7 +64,7 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
ns = ""
}
spec := NewGVR(gvr)
res := spec.AsGVR()
res := spec.GVR()
return &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{

View File

@ -72,7 +72,7 @@ func (g GVR) String() string {
}
// AsGV returns the group version scheme representation.
func (g GVR) AsGV() schema.GroupVersion {
func (g GVR) GV() schema.GroupVersion {
return schema.GroupVersion{
Group: g.g,
Version: g.v,
@ -80,39 +80,39 @@ func (g GVR) AsGV() schema.GroupVersion {
}
// AsGVR returns a a full schema representation.
func (g GVR) AsGVR() schema.GroupVersionResource {
func (g GVR) GVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: g.ToG(),
Version: g.ToV(),
Resource: g.ToR(),
Group: g.G(),
Version: g.V(),
Resource: g.R(),
}
}
// AsGR returns a a full schema representation.
func (g GVR) AsGR() *schema.GroupResource {
func (g GVR) GR() *schema.GroupResource {
return &schema.GroupResource{
Group: g.ToG(),
Resource: g.ToR(),
Group: g.G(),
Resource: g.R(),
}
}
// ToV returns the resource version.
func (g GVR) ToV() string {
func (g GVR) V() string {
return g.v
}
// ToRAndG returns the resource and group.
func (g GVR) ToRAndG() (string, string) {
func (g GVR) RG() (string, string) {
return g.r, g.g
}
// ToR returns the resource name.
func (g GVR) ToR() string {
func (g GVR) R() string {
return g.r
}
// ToG returns the resource group name.
func (g GVR) ToG() string {
func (g GVR) G() string {
return g.g
}
@ -131,7 +131,7 @@ func (g GVRs) Swap(i, j int) {
// Less returns true if i < j.
func (g GVRs) Less(i, j int) bool {
g1, g2 := g[i].ToG(), g[j].ToG()
g1, g2 := g[i].G(), g[j].G()
return sortorder.NaturalLess(g1, g2)
}

View File

@ -59,7 +59,7 @@ func TestAsGVR(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, client.NewGVR(u.gvr).AsGVR())
assert.Equal(t, u.e, client.NewGVR(u.gvr).GVR())
})
}
}
@ -77,7 +77,7 @@ func TestAsGV(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, client.NewGVR(u.gvr).AsGV())
assert.Equal(t, u.e, client.NewGVR(u.gvr).GV())
})
}
}
@ -132,7 +132,7 @@ func TestToR(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, client.NewGVR(u.gvr).ToR())
assert.Equal(t, u.e, client.NewGVR(u.gvr).R())
})
}
}
@ -151,7 +151,7 @@ func TestToG(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, client.NewGVR(u.gvr).ToG())
assert.Equal(t, u.e, client.NewGVR(u.gvr).G())
})
}
}
@ -170,7 +170,7 @@ func TestToV(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, client.NewGVR(u.gvr).ToV())
assert.Equal(t, u.e, client.NewGVR(u.gvr).V())
})
}
}

View File

@ -17,7 +17,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error)
return "", err
}
gvk, err := m.KindFor(gvr.AsGVR())
gvk, err := m.KindFor(gvr.GVR())
if err != nil {
log.Error().Err(err).Msgf("No GVK for resource %s", gvr)
return "", err

View File

@ -26,7 +26,6 @@ type Generic struct {
// List returns a collection of resources.
// BOZO!! no auth check??
func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) {
log.Debug().Msgf("GENERIC-LIST %q:%q", ns, g.gvr)
labelSel, ok := ctx.Value(internal.KeyLabels).(string)
if !ok {
log.Warn().Msgf("No label selector found in context. Listing all resources")
@ -116,5 +115,5 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
}
func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface {
return g.Client().DynDialOrDie().Resource(g.gvr.AsGVR())
return g.Client().DynDialOrDie().Resource(g.gvr.GVR())
}

View File

@ -42,7 +42,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {
}
res := client.NewGVR(gvr)
switch res.ToR() {
switch res.R() {
case "clusterrolebindings":
return r.loadClusterRoleBinding(path)
case "rolebindings":
@ -52,7 +52,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {
case "roles":
return r.loadRole(path)
default:
return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.ToR())
return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.R())
}
}

View File

@ -3,6 +3,7 @@ package dao
import (
"fmt"
"sort"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
@ -115,22 +116,26 @@ func loadK9s(m ResourceMetas) {
m[client.NewGVR("xrays")] = metav1.APIResource{
Name: "xray",
Kind: "XRays",
SingularName: "xray",
Categories: []string{"k9s"},
}
m[client.NewGVR("aliases")] = metav1.APIResource{
Name: "aliases",
Kind: "Aliases",
SingularName: "alias",
Categories: []string{"k9s"},
}
m[client.NewGVR("contexts")] = metav1.APIResource{
Name: "contexts",
Kind: "Contexts",
SingularName: "context",
ShortNames: []string{"ctx"},
Categories: []string{"k9s"},
}
m[client.NewGVR("screendumps")] = metav1.APIResource{
Name: "screendumps",
Kind: "ScreenDumps",
SingularName: "screendump",
ShortNames: []string{"sd"},
Verbs: []string{"delete"},
Categories: []string{"k9s"},
@ -138,6 +143,7 @@ func loadK9s(m ResourceMetas) {
m[client.NewGVR("benchmarks")] = metav1.APIResource{
Name: "benchmarks",
Kind: "Benchmarks",
SingularName: "benchmark",
ShortNames: []string{"be"},
Verbs: []string{"delete"},
Categories: []string{"k9s"},
@ -146,6 +152,7 @@ func loadK9s(m ResourceMetas) {
Name: "portforwards",
Namespaced: true,
Kind: "PortForwards",
SingularName: "portforward",
ShortNames: []string{"pf"},
Verbs: []string{"delete"},
Categories: []string{"k9s"},
@ -153,6 +160,7 @@ func loadK9s(m ResourceMetas) {
m[client.NewGVR("containers")] = metav1.APIResource{
Name: "containers",
Kind: "Containers",
SingularName: "container",
Categories: []string{"k9s"},
}
}
@ -199,7 +207,10 @@ func loadPreferred(f Factory, m ResourceMetas) error {
for _, r := range rr {
for _, res := range r.APIResources {
gvr := client.FromGVAndR(r.GroupVersion, res.Name)
res.Group, res.Version = gvr.ToG(), gvr.ToV()
res.Group, res.Version = gvr.G(), gvr.V()
if res.SingularName == "" {
res.SingularName = strings.ToLower(res.Kind)
}
m[gvr] = res
}
}

View File

@ -34,7 +34,7 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
SetHeader("Accept", a).
Namespace(ns).
Name(n).
Resource(t.gvr.ToR()).
Resource(t.gvr.R()).
VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get()
if err != nil {
@ -57,7 +57,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
o, err := c.Get().
SetHeader("Accept", a).
Namespace(ns).
Resource(t.gvr.ToR()).
Resource(t.gvr.R()).
VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get()
if err != nil {
@ -74,10 +74,10 @@ const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
func (t *Table) getClient() (*rest.RESTClient, error) {
crConfig := t.Client().RestConfigOrDie()
gv := t.gvr.AsGV()
gv := t.gvr.GV()
crConfig.GroupVersion = &gv
crConfig.APIPath = "/apis"
if t.gvr.ToG() == "" {
if t.gvr.G() == "" {
crConfig.APIPath = "/api"
}
codec, _ := t.codec()
@ -92,7 +92,7 @@ func (t *Table) getClient() (*rest.RESTClient, error) {
func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
scheme := runtime.NewScheme()
gv := t.gvr.AsGV()
gv := t.gvr.GV()
metav1.AddToGroupVersion(scheme, gv)
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})

View File

@ -98,6 +98,7 @@ var Registry = map[string]ResourceMeta{
},
"apps/v1/replicasets": {
Renderer: &render.ReplicaSet{},
TreeRenderer: &xray.ReplicaSet{},
},
"apps/v1/statefulsets": {
DAO: &dao.StatefulSet{},

View File

@ -15,7 +15,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
const iniRefreshRate = 300 * time.Millisecond
const initRefreshRate = 300 * time.Millisecond
// TableListener represents a table model listener.
type TableListener interface {
@ -176,7 +176,7 @@ func (t *Table) Peek() render.TableData {
func (t *Table) updater(ctx context.Context) {
defer log.Debug().Msgf("Model canceled -- %q", t.gvr)
rate := iniRefreshRate
rate := initRefreshRate
for {
select {
case <-ctx.Done():

View File

@ -18,6 +18,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
const initTreeRefreshRate = 500 * time.Millisecond
// TreeListener represents a tree model listener.
type TreeListener interface {
// TreeChanged notifies the model data changed.
@ -159,7 +161,7 @@ func (t *Tree) ToYAML(ctx context.Context, gvr, path string) (string, error) {
func (t *Tree) updater(ctx context.Context) {
defer log.Debug().Msgf("Tree-model canceled -- %q", t.gvr)
rate := iniRefreshRate
rate := initTreeRefreshRate
for {
select {
case <-ctx.Done():
@ -204,7 +206,8 @@ func (t *Tree) reconcile(ctx context.Context) error {
}
ns := client.CleanseNamespace(t.namespace)
root := xray.NewTreeNode("root", client.NewGVR(t.gvr).ToR())
res := client.NewGVR(t.gvr).R()
root := xray.NewTreeNode(res, res)
ctx = context.WithValue(ctx, xray.KeyParent, root)
if _, ok := meta.TreeRenderer.(*xray.Generic); ok {
table, ok := oo[0].(*metav1beta1.Table)
@ -287,6 +290,9 @@ func rxFilter(q, path string) bool {
}
func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRenderer) error {
if re == nil {
return fmt.Errorf("no tree renderer defined for this resource")
}
for _, o := range oo {
if err := re.Render(ctx, ns, o); err != nil {
return err

View File

@ -39,7 +39,7 @@ func (Alias) Render(o interface{}, ns string, r *Row) error {
r.ID = a.GVR
gvr := client.NewGVR(a.GVR)
res, grp := gvr.ToRAndG()
res, grp := gvr.RG()
r.Fields = append(r.Fields,
res,
strings.Join(a.Aliases, ","),

View File

@ -23,7 +23,7 @@ const (
// FlashFatal represents an fatal message.
FlashFatal
flashDelay = 3
flashDelay = 3 * time.Second
emoDoh = "😗"
emoRed = "😡"
@ -41,21 +41,30 @@ type (
cancel context.CancelFunc
app *App
flushNow bool
}
)
// NewFlash returns a new flash view.
func NewFlash(app *App, m string) *Flash {
f := Flash{app: app, TextView: tview.NewTextView()}
f := Flash{
app: app,
TextView: tview.NewTextView(),
}
f.SetTextColor(tcell.ColorAqua)
f.SetTextAlign(tview.AlignLeft)
f.SetBorderPadding(0, 0, 1, 1)
f.SetText("")
f.SetText(m)
f.app.Styles.AddListener(&f)
return &f
}
// TestMode for testing...
func (f *Flash) TestMode() {
f.flushNow = true
}
// StylesChanged notifies listener the skin changed.
func (f *Flash) StylesChanged(s *config.Styles) {
f.SetBackgroundColor(s.BgColor())
@ -64,6 +73,7 @@ func (f *Flash) StylesChanged(s *config.Styles) {
// Info displays an info flash message.
func (f *Flash) Info(msg string) {
log.Info().Msg(msg)
f.SetMessage(FlashInfo, msg)
}
@ -74,6 +84,7 @@ func (f *Flash) Infof(fmat string, args ...interface{}) {
// Warn displays a warning flash message.
func (f *Flash) Warn(msg string) {
log.Warn().Msg(msg)
f.SetMessage(FlashWarn, msg)
}
@ -84,7 +95,7 @@ func (f *Flash) Warnf(fmat string, args ...interface{}) {
// Err displays an error flash message.
func (f *Flash) Err(err error) {
log.Error().Err(err).Msgf("%v", err)
log.Error().Msg(err.Error())
f.SetMessage(FlashErr, err.Error())
}
@ -106,35 +117,35 @@ func (f *Flash) SetMessage(level FlashLevel, msg ...string) {
if f.cancel != nil {
f.cancel()
}
var ctx1, ctx2 context.Context
{
var timerCancel context.CancelFunc
ctx1, f.cancel = context.WithCancel(context.TODO())
ctx2, timerCancel = context.WithTimeout(context.TODO(), flashDelay*time.Second)
go f.refresh(ctx1, ctx2, timerCancel)
}
_, _, width, _ := f.GetRect()
if width <= 15 {
width = 100
}
m := strings.Join(msg, " ")
if f.flushNow {
f.SetTextColor(flashColor(level))
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
} else {
f.app.QueueUpdateDraw(func() {
log.Debug().Msgf("FLASH %q", m)
f.SetTextColor(flashColor(level))
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
})
}
func (f *Flash) refresh(ctx1, ctx2 context.Context, cancel context.CancelFunc) {
defer cancel()
for {
select {
case <-ctx1.Done():
return
case <-ctx2.Done():
var ctx context.Context
ctx, f.cancel = context.WithCancel(context.TODO())
ctx, f.cancel = context.WithTimeout(ctx, flashDelay)
go f.refresh(ctx)
}
func (f *Flash) refresh(ctx context.Context) {
<-ctx.Done()
f.app.QueueUpdateDraw(func() {
log.Debug().Msgf("FLASH-CLEAR %q", f.GetText(true))
f.Clear()
})
return
}
}
}
func flashEmoji(l FlashLevel) string {

View File

@ -9,32 +9,37 @@ import (
)
func TestFlashInfo(t *testing.T) {
f := ui.NewFlash(ui.NewApp(""), "YO!")
f := newFlash()
f.Info("Blee")
assert.Equal(t, "😎 Blee\n", f.GetText(false))
assert.Equal(t, "😎 Blee\n", f.GetText(false))
f.Infof("Blee %s", "duh")
assert.Equal(t, "😎 Blee duh\n", f.GetText(false))
}
func TestFlashWarn(t *testing.T) {
f := ui.NewFlash(ui.NewApp(""), "YO!")
f := newFlash()
f.Warn("Blee")
assert.Equal(t, "😗 Blee\n", f.GetText(false))
assert.Equal(t, "😗 Blee\n", f.GetText(false))
f.Warnf("Blee %s", "duh")
assert.Equal(t, "😗 Blee duh\n", f.GetText(false))
}
func TestFlashErr(t *testing.T) {
f := ui.NewFlash(ui.NewApp(""), "YO!")
f := newFlash()
f.Err(errors.New("Blee"))
assert.Equal(t, "😡 Blee\n", f.GetText(false))
f.Errf("Blee %s", "duh")
assert.Equal(t, "😡 Blee duh\n", f.GetText(false))
}
// ----------------------------------------------------------------------------
// Helpers...
func newFlash() *ui.Flash {
f := ui.NewFlash(ui.NewApp(""), "YO!")
f.TestMode()
return f
}

View File

@ -314,10 +314,11 @@ func (a *App) Status(l ui.FlashLevel, msg string) {
a.Draw()
}
// ClearStatus reset log back to normal.
// ClearStatus reset logo back to normal.
func (a *App) ClearStatus(flash bool) {
a.Logo().Reset()
if flash {
log.Debug().Msgf("FLASH CLEARED!!")
a.Flash().Clear()
}
a.Draw()

View File

@ -92,7 +92,6 @@ func (b *Browser) SetInstance(path string) {
func (b *Browser) Start() {
b.Stop()
b.App().Status(ui.FlashInfo, "Loading...")
b.Table.Start()
ctx := b.defaultContext()
ctx, b.cancelFn = context.WithCancel(ctx)
@ -141,7 +140,7 @@ func (b *Browser) TableDataChanged(data render.TableData) {
b.app.QueueUpdateDraw(func() {
b.refreshActions()
b.Update(data)
b.App().ClearStatus(true)
b.App().ClearStatus(false)
})
}
@ -244,7 +243,7 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
b.Stop()
defer b.Start()
{
msg := fmt.Sprintf("Delete %s %s?", b.gvr.ToR(), selections[0])
msg := fmt.Sprintf("Delete %s %s?", b.gvr.R(), selections[0])
if len(selections) > 1 {
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.gvr)
}

View File

@ -43,13 +43,32 @@ func (c *Command) Init() error {
return nil
}
func allowedXRay(gvr client.GVR) bool {
gg := []string{
"v1/pods",
"v1/services",
"apps/v1/deployments",
"apps/v1/daemonsets",
"apps/v1/statefulsets",
"apps/v1/replicasets",
}
for _, g := range gg {
if g == gvr.String() {
return true
}
}
return false
}
func (c *Command) xrayCmd(cmd string) error {
tokens := strings.Split(cmd, " ")
if len(tokens) < 2 {
return errors.New("You must specify a resource")
}
gvr, ok := c.alias.AsGVR(tokens[1])
if !ok {
if !ok || !allowedXRay(gvr) {
return fmt.Errorf("Huh? `%s` Command not found", cmd)
}
return c.exec(cmd, "xrays", NewXray(gvr), true)
@ -172,8 +191,7 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) e
if comp == nil {
return fmt.Errorf("No component given for %s", gvr)
}
c.app.Flash().Infof("Running command %s", cmd)
c.app.Flash().Infof("Viewing %s...", client.NewGVR(gvr).R())
c.app.Config.SetActiveView(cmd)
if err := c.app.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")

View File

@ -65,7 +65,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
}
sel := n.GetTable().GetSelectedItem()
gvr := client.NewGVR(n.GVR()).AsGVR()
gvr := client.NewGVR(n.GVR()).GVR()
o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(sel, metav1.GetOptions{})
if err != nil {
n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err)

View File

@ -77,9 +77,9 @@ func (x *Xray) Init(ctx context.Context) error {
x.SetBackgroundColor(config.AsColor(x.app.Styles.GetTable().BgColor))
x.SetBorderColor(config.AsColor(x.app.Styles.GetTable().FgColor))
x.SetBorderFocusColor(config.AsColor(x.app.Styles.Frame().Border.FocusColor))
x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.ToR())))
x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.R())))
x.SetGraphics(true)
x.SetGraphicsColor(tcell.ColorDimGray)
x.SetGraphicsColor(tcell.ColorFloralWhite)
x.SetInputCapture(x.keyboard)
x.model.SetRefreshRate(time.Duration(x.app.Config.K9s.GetRefreshRate()) * time.Second)
@ -370,7 +370,7 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey {
ns, n := client.Namespaced(ref.Path)
args := make([]string, 0, 10)
args = append(args, "edit")
args = append(args, client.NewGVR(ref.GVR).ToR())
args = append(args, client.NewGVR(ref.GVR).R())
args = append(args, "-n", ns)
args = append(args, "--context", x.app.Config.K9s.CurrentContext)
if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
@ -450,7 +450,7 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if len(strings.Split(ref.Path, "/")) == 1 {
return nil
}
if err := x.app.viewResource(client.NewGVR(ref.GVR).ToR(), ref.Path, false); err != nil {
if err := x.app.viewResource(client.NewGVR(ref.GVR).R(), ref.Path, false); err != nil {
x.app.Flash().Err(err)
}
@ -632,11 +632,14 @@ func (x *Xray) App() *App {
// UpdateTitle updates the view title.
func (x *Xray) UpdateTitle() {
x.SetTitle(x.styleTitle())
t := x.styleTitle()
x.app.QueueUpdateDraw(func() {
x.SetTitle(t)
})
}
func (x *Xray) styleTitle() string {
base := fmt.Sprintf("%s-%s", xrayTitle, strings.Title(x.gvr.ToR()))
base := fmt.Sprintf("%s-%s", xrayTitle, strings.Title(x.gvr.R()))
ns := x.model.GetNamespace()
if client.IsAllNamespaces(ns) {
ns = client.NamespaceAll

View File

@ -38,6 +38,9 @@ func NewFactory(client client.Connection) *Factory {
// Start initializes the informers until caller cancels the context.
func (f *Factory) Start(ns string) {
f.mx.Lock()
defer f.mx.Unlock()
log.Debug().Msgf("Factory START with ns `%q", ns)
f.stopChan = make(chan struct{})
for ns, fac := range f.factories {
@ -48,13 +51,13 @@ func (f *Factory) Start(ns string) {
// Terminate terminates all watchers and forwards.
func (f *Factory) Terminate() {
f.mx.Lock()
defer f.mx.Unlock()
if f.stopChan != nil {
close(f.stopChan)
f.stopChan = nil
}
f.mx.Lock()
defer f.mx.Unlock()
for k := range f.factories {
delete(f.factories, k)
}
@ -179,6 +182,9 @@ func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer {
log.Error().Err(fmt.Errorf("MEOW! No informer for %q:%q", ns, gvr))
return inf
}
f.mx.RLock()
defer f.mx.RUnlock()
fact.Start(f.stopChan)
return inf
@ -193,7 +199,6 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory {
if fac, ok := f.factories[ns]; ok {
return fac
}
log.Debug().Msgf("FACTORY_CREATE for ns %q", ns)
f.factories[ns] = di.NewFilteredDynamicSharedInformerFactory(
f.client.DynDialOrDie(),
defaultResync,

80
internal/xray/rs.go Normal file
View File

@ -0,0 +1,80 @@
package xray
import (
"context"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
// ReplicaSet represents an xray renderer.
type ReplicaSet struct{}
// Render renders an xray node.
func (r *ReplicaSet) Render(ctx context.Context, ns string, o interface{}) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Unstructured, but got %T", o)
}
var rs appsv1.ReplicaSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs)
if err != nil {
return err
}
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
root := NewTreeNode("apps/v1/replicasets", client.FQN(rs.Namespace, rs.Name))
oo, err := locatePods(ctx, rs.Namespace, rs.Spec.Selector)
if err != nil {
return err
}
ctx = context.WithValue(ctx, KeyParent, root)
var re Pod
for _, o := range oo {
p, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expecting *Unstructured but got %T", o)
}
if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {
return err
}
}
if root.IsLeaf() {
return nil
}
gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, rs.Namespace)
nsn := parent.Find(gvr, nsID)
if nsn == nil {
nsn = NewTreeNode(gvr, nsID)
parent.Add(nsn)
}
nsn.Add(root)
return r.validate(root, rs)
}
func (*ReplicaSet) validate(root *TreeNode, rs appsv1.ReplicaSet) error {
root.Extras[StatusKey] = OkStatus
var r int32
if rs.Spec.Replicas != nil {
r = int32(*rs.Spec.Replicas)
}
a := rs.Status.Replicas
if a != r {
root.Extras[StatusKey] = ToastStatus
}
root.Extras[InfoKey] = fmt.Sprintf("%d/%d", a, r)
return nil
}

44
internal/xray/rs_test.go Normal file
View File

@ -0,0 +1,44 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestReplicaSetRender(t *testing.T) {
uu := map[string]struct {
file string
level1, level2 int
status string
}{
"plain": {
file: "rs",
level1: 1,
level2: 1,
status: xray.OkStatus,
},
}
var re xray.ReplicaSet
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
f := makeFactory()
f.rows = map[string][]runtime.Object{"v1/pods": {load(t, "po")}}
o := load(t, u.file)
root := xray.NewTreeNode("replicasets", "replicasets")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, f)
assert.Nil(t, re.Render(ctx, "", o))
assert.Equal(t, u.level1, root.CountChildren())
assert.Equal(t, u.level2, root.Children[0].CountChildren())
})
}
}

View File

@ -37,7 +37,7 @@ func (s *Service) Render(ctx context.Context, ns string, o interface{}) error {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
root := NewTreeNode("apps/v1/services", client.FQN(svc.Namespace, svc.Name))
root := NewTreeNode("v1/services", client.FQN(svc.Namespace, svc.Name))
oo, err := s.locatePods(ctx, svc.Namespace, svc.Spec.Selector)
if err != nil {
return err

View File

@ -0,0 +1,122 @@
{
"apiVersion": "apps/v1",
"kind": "ReplicaSet",
"metadata": {
"annotations": {
"deployment.kubernetes.io/desired-replicas": "1",
"deployment.kubernetes.io/max-replicas": "2",
"deployment.kubernetes.io/revision": "2"
},
"creationTimestamp": "2020-01-20T01:34:11Z",
"generation": 1,
"labels": {
"app": "nginx-pv",
"pod-template-hash": "6476d7d5c8"
},
"name": "nginx-pv-6476d7d5c8",
"namespace": "default",
"ownerReferences": [
{
"apiVersion": "apps/v1",
"blockOwnerDeletion": true,
"controller": true,
"kind": "Deployment",
"name": "nginx-pv",
"uid": "68aa70ff-ff7c-4a67-8d4f-fc31ef27ec35"
}
],
"resourceVersion": "3743997",
"selfLink": "/apis/apps/v1/namespaces/default/replicasets/nginx-pv-6476d7d5c8",
"uid": "547a036d-94d9-4818-bd9e-ec2939019471"
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "nginx-pv",
"pod-template-hash": "6476d7d5c8"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "nginx-pv",
"pod-template-hash": "6476d7d5c8"
}
},
"spec": {
"automountServiceAccountToken": true,
"containers": [
{
"env": [
{
"name": "FRED",
"valueFrom": {
"configMapKeyRef": {
"key": "fred",
"name": "busy"
}
}
},
{
"name": "PROPS",
"valueFrom": {
"configMapKeyRef": {
"key": "props",
"name": "busy"
}
}
}
],
"image": "k8s.gcr.io/nginx-slim:0.8",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {
"limits": {
"cpu": "100m",
"memory": "200Mi"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/usr/share/nginx/html",
"name": "index"
}
]
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"serviceAccount": "zorg",
"serviceAccountName": "zorg",
"terminationGracePeriodSeconds": 30,
"volumes": [
{
"name": "index",
"persistentVolumeClaim": {
"claimName": "web"
}
}
]
}
}
},
"status": {
"availableReplicas": 1,
"fullyLabeledReplicas": 1,
"observedGeneration": 1,
"readyReplicas": 1,
"replicas": 1
}
}

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log"
"vbom.ml/util/sortorder"
)
@ -295,15 +296,7 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode {
// Title computes the node title.
func (t *TreeNode) Title() string {
const withNS = "[white::b]%s[-::d]"
title := fmt.Sprintf(withNS, t.AsString())
if t.CountChildren() > 0 {
title += fmt.Sprintf("([white::d]%d[-::d])[-::-]", t.CountChildren())
}
return title
return t.toTitle()
}
// ----------------------------------------------------------------------------
@ -341,53 +334,55 @@ func dumpStdOut(n *TreeNode, level int) {
}
}
func toEmoji(gvr string) string {
switch gvr {
case "v1/pods":
return "🚛"
case "apps/v1/deployments":
return "🪂"
case "apps/v1/statefulset":
return "🎎"
case "apps/v1/daemonsets":
return "😈"
case "containers":
return "🐳"
case "v1/serviceaccounts":
return "💁‍♀️"
case "v1/persistentvolumes":
return "📚"
case "v1/persistentvolumeclaims":
return "🎟"
case "v1/secrets":
return "🔒"
case "v1/configmaps":
return "🔑"
default:
return "📎"
}
func category(gvr string) string {
meta, err := dao.MetaFor(client.NewGVR(gvr))
if err != nil {
return ""
}
// AsString transforms a node as a string for viewing.
func (t TreeNode) AsString() string {
const colorFmt = "%s [gray::-][%s[gray::-]] [%s::b]%s[::]"
return meta.SingularName
}
const (
titleFmt = " [gray::-]%s/[white::b][%s::b]%s[::]"
topTitleFmt = " [white::b][%s::b]%s[::]"
toast = "TOAST"
)
func (t TreeNode) toTitle() (title string) {
_, n := client.Namespaced(t.ID)
color, flag := "white", "[green::b]OK"
color, status := "white", "OK"
if v, ok := t.Extras[StatusKey]; ok {
switch v {
case ToastStatus:
color, flag = "orangered", "[red::b]TOAST"
color, status = "orangered", toast
case MissingRefStatus:
color, flag = "orange", "[orange::b]TOAST_REF"
color, status = "orange", toast+"_REF"
}
}
str := fmt.Sprintf(colorFmt, toEmoji(t.GVR), flag, color, n)
i, ok := t.Extras[InfoKey]
defer func() {
if status != "OK" {
title += fmt.Sprintf(" [gray::-][[%s::b]%s[gray::-]]", color, status)
}
}()
categ := category(t.GVR)
if categ == "" {
title = fmt.Sprintf(topTitleFmt, color, n)
} else {
title = fmt.Sprintf(titleFmt, categ, color, n)
}
if !t.IsLeaf() {
title += fmt.Sprintf("[white::d](%d[-::d])[-::-]", t.CountChildren())
}
info, ok := t.Extras[InfoKey]
if !ok {
return str
return
}
return fmt.Sprintf("%s [antiquewhite::][%s][::] ", str, i)
title += fmt.Sprintf(" [antiquewhite::][%s][::]", info)
return
}