allow jumping to the owner of the resource (#2700)
parent
2fca1a1655
commit
f802d3948a
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -123,6 +124,20 @@ func (m *Meta) AllGVRs() client.GVRs {
|
||||||
return kk
|
return kk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GVK2GVR convert gvk to gvr
|
||||||
|
func (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (client.GVR, bool) {
|
||||||
|
m.mx.RLock()
|
||||||
|
defer m.mx.RUnlock()
|
||||||
|
|
||||||
|
for gvr, meta := range m.resMetas {
|
||||||
|
if gv.Group == meta.Group && gv.Version == meta.Version && kind == meta.Kind {
|
||||||
|
return gvr, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.NoGVR, false
|
||||||
|
}
|
||||||
|
|
||||||
// IsCRD checks if resource represents a CRD
|
// IsCRD checks if resource represents a CRD
|
||||||
func IsCRD(r metav1.APIResource) bool {
|
func IsCRD(r metav1.APIResource) bool {
|
||||||
for _, c := range r.Categories {
|
for _, c := range r.Categories {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/ui"
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectAction func(index int)
|
||||||
|
|
||||||
|
func ShowSelection(styles config.Dialog, pages *ui.Pages, title string, options []string, action SelectAction) {
|
||||||
|
list := tview.NewList()
|
||||||
|
list.ShowSecondaryText(false)
|
||||||
|
list.SetSelectedTextColor(styles.ButtonFocusFgColor.Color())
|
||||||
|
list.SetSelectedBackgroundColor(styles.ButtonFocusBgColor.Color())
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
list.AddItem(option, "", 0, nil)
|
||||||
|
list.AddItem(option, "", 0, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
modal := ui.NewModalList("<"+title+">", list)
|
||||||
|
modal.SetDoneFunc(func(i int, s string) {
|
||||||
|
dismiss(pages)
|
||||||
|
action(i)
|
||||||
|
})
|
||||||
|
|
||||||
|
pages.AddPage(dialogKey, modal, false, false)
|
||||||
|
pages.ShowPage(dialogKey)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/derailed/tcell/v2"
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModalList struct {
|
||||||
|
*tview.Box
|
||||||
|
|
||||||
|
// The list embedded in the modal's frame.
|
||||||
|
list *tview.List
|
||||||
|
|
||||||
|
// The frame embedded in the modal.
|
||||||
|
frame *tview.Frame
|
||||||
|
|
||||||
|
// The optional callback for when the user clicked one of the items. It
|
||||||
|
// receives the index of the clicked item and the item's text.
|
||||||
|
done func(int, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModalList(title string, list *tview.List) *ModalList {
|
||||||
|
m := &ModalList{Box: tview.NewBox()}
|
||||||
|
|
||||||
|
m.list = list
|
||||||
|
m.list.SetBackgroundColor(tview.Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0)
|
||||||
|
m.list.SetSelectedFunc(func(i int, main string, _ string, _ rune) {
|
||||||
|
if m.done != nil {
|
||||||
|
m.done(i, main)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
m.list.SetDoneFunc(func() {
|
||||||
|
if m.done != nil {
|
||||||
|
m.done(-1, "")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
m.frame = tview.NewFrame(m.list).SetBorders(0, 0, 1, 0, 0, 0)
|
||||||
|
m.frame.SetBorder(true).
|
||||||
|
SetBackgroundColor(tview.Styles.ContrastBackgroundColor).
|
||||||
|
SetBorderPadding(1, 1, 1, 1)
|
||||||
|
m.frame.SetTitle(title)
|
||||||
|
m.frame.SetTitleColor(tcell.ColorAqua)
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw draws this primitive onto the screen.
|
||||||
|
func (m *ModalList) Draw(screen tcell.Screen) {
|
||||||
|
// Calculate the width of this modal.
|
||||||
|
width := 0
|
||||||
|
for i := 0; i < m.list.GetItemCount(); i++ {
|
||||||
|
main, secondary := m.list.GetItemText(i)
|
||||||
|
width = max(width, len(main)+len(secondary)+2)
|
||||||
|
}
|
||||||
|
|
||||||
|
screenWidth, screenHeight := screen.Size()
|
||||||
|
|
||||||
|
// Set the modal's position and size.
|
||||||
|
height := m.list.GetItemCount() + 4
|
||||||
|
width += 2
|
||||||
|
x := (screenWidth - width) / 2
|
||||||
|
y := (screenHeight - height) / 2
|
||||||
|
m.SetRect(x, y, width, height)
|
||||||
|
|
||||||
|
// Draw the frame.
|
||||||
|
m.frame.SetRect(x, y, width, height)
|
||||||
|
m.frame.Draw(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ModalList) SetDoneFunc(handler func(int, string)) *ModalList {
|
||||||
|
m.done = handler
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus is called when this primitive receives focus.
|
||||||
|
func (m *ModalList) Focus(delegate func(p tview.Primitive)) {
|
||||||
|
delegate(m.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasFocus returns whether this primitive has focus.
|
||||||
|
func (m *ModalList) HasFocus() bool {
|
||||||
|
return m.list.HasFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseHandler returns the mouse handler for this primitive.
|
||||||
|
func (m *ModalList) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
|
||||||
|
return m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
|
||||||
|
// Pass mouse events on to the form.
|
||||||
|
consumed, capture = m.list.MouseHandler()(action, event, setFocus)
|
||||||
|
if !consumed && action == tview.MouseLeftClick && m.InRect(event.Position()) {
|
||||||
|
setFocus(m)
|
||||||
|
consumed = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputHandler returns the handler for this primitive.
|
||||||
|
func (m *ModalList) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||||
|
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||||
|
if m.frame.HasFocus() {
|
||||||
|
if handler := m.frame.InputHandler(); handler != nil {
|
||||||
|
handler(event, setFocus)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/model"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -32,7 +31,7 @@ func NewPages() *Pages {
|
||||||
func (p *Pages) IsTopDialog() bool {
|
func (p *Pages) IsTopDialog() bool {
|
||||||
_, pa := p.GetFrontPage()
|
_, pa := p.GetFrontPage()
|
||||||
switch pa.(type) {
|
switch pa.(type) {
|
||||||
case *tview.ModalForm:
|
case *tview.ModalForm, *ModalList:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ func TestHelp(t *testing.T) {
|
||||||
v := view.NewHelp(app)
|
v := view.NewHelp(app)
|
||||||
|
|
||||||
assert.Nil(t, v.Init(ctx))
|
assert.Nil(t, v.Init(ctx))
|
||||||
assert.Equal(t, 28, v.GetRowCount())
|
assert.Equal(t, 29, v.GetRowCount())
|
||||||
assert.Equal(t, 8, v.GetColumnCount())
|
assert.Equal(t, 8, v.GetColumnCount())
|
||||||
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
|
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
|
||||||
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))
|
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ func NewJob(gvr client.GVR) ResourceViewer {
|
||||||
var j Job
|
var j Job
|
||||||
|
|
||||||
j.ResourceViewer = NewVulnerabilityExtender(
|
j.ResourceViewer = NewVulnerabilityExtender(
|
||||||
NewLogsExtender(NewBrowser(gvr), j.logOptions),
|
NewOwnerExtender(
|
||||||
|
NewLogsExtender(NewBrowser(gvr), j.logOptions),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
j.GetTable().SetEnterFn(j.showPods)
|
j.GetTable().SetEnterFn(j.showPods)
|
||||||
j.GetTable().SetSortCol("AGE", true)
|
j.GetTable().SetSortCol("AGE", true)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/derailed/k9s/internal/ui/dialog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/derailed/tcell/v2"
|
||||||
|
"github.com/go-errors/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/derailed/k9s/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OwnerExtender adds owner actions to a given viewer.
|
||||||
|
type OwnerExtender struct {
|
||||||
|
ResourceViewer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOwnerExtender returns a new extender.
|
||||||
|
func NewOwnerExtender(r ResourceViewer) ResourceViewer {
|
||||||
|
v := &OwnerExtender{ResourceViewer: r}
|
||||||
|
v.AddBindKeysFn(v.bindKeys)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OwnerExtender) bindKeys(aa *ui.KeyActions) {
|
||||||
|
aa.Add(ui.KeyShiftJ, ui.NewKeyAction("Jump Owner", v.ownerCmd, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OwnerExtender) ownerCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
path := v.GetTable().GetSelectedItem()
|
||||||
|
if path == "" {
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.findOwnerFor(path); err != nil {
|
||||||
|
log.Warn().Msgf("Unable to jump to the owner of resource %q: %s", path, err)
|
||||||
|
v.App().Flash().Warnf("Unable to jump owner: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OwnerExtender) findOwnerFor(path string) error {
|
||||||
|
res, err := dao.AccessorFor(v.App().factory, v.GVR())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o, err := res.Get(v.defaultCtx(), path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u, ok := v.asUnstructuredObject(o)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("unsupported object type: %t", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, _ := client.Namespaced(path)
|
||||||
|
ownerReferences := u.GetOwnerReferences()
|
||||||
|
if len(ownerReferences) == 1 {
|
||||||
|
return v.jumpOwner(ns, ownerReferences[0])
|
||||||
|
} else if len(ownerReferences) > 1 {
|
||||||
|
owners := make([]string, 0, len(ownerReferences))
|
||||||
|
for idx, ownerRef := range ownerReferences {
|
||||||
|
owners = append(owners, fmt.Sprintf("%d: %s", idx, ownerRef.Kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.ShowSelection(v.App().Styles.Dialog(), v.App().Content.Pages, "Jump To", owners, func(index int) {
|
||||||
|
if index >= 0 {
|
||||||
|
err = v.jumpOwner(ns, ownerReferences[index])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Errorf("no owner found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OwnerExtender) jumpOwner(ns string, owner metav1.OwnerReference) error {
|
||||||
|
gv, err := schema.ParseGroupVersion(owner.APIVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gvr, found := dao.MetaAccess.GVK2GVR(gv, owner.Kind)
|
||||||
|
if !found {
|
||||||
|
return errors.Errorf("unsupported GVK: %s/%s", owner.APIVersion, owner.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.App().gotoResource(gvr.String(), client.FQN(ns, owner.Name), false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OwnerExtender) defaultCtx() context.Context {
|
||||||
|
return context.WithValue(context.Background(), internal.KeyFactory, v.App().factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OwnerExtender) asUnstructuredObject(o runtime.Object) (*unstructured.Unstructured, bool) {
|
||||||
|
switch v := o.(type) {
|
||||||
|
case *unstructured.Unstructured:
|
||||||
|
return v, true
|
||||||
|
case *render.PodWithMetrics:
|
||||||
|
return v.Raw, true
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,9 +50,11 @@ type Pod struct {
|
||||||
func NewPod(gvr client.GVR) ResourceViewer {
|
func NewPod(gvr client.GVR) ResourceViewer {
|
||||||
var p Pod
|
var p Pod
|
||||||
p.ResourceViewer = NewPortForwardExtender(
|
p.ResourceViewer = NewPortForwardExtender(
|
||||||
NewVulnerabilityExtender(
|
NewOwnerExtender(
|
||||||
NewImageExtender(
|
NewVulnerabilityExtender(
|
||||||
NewLogsExtender(NewBrowser(gvr), p.logOptions),
|
NewImageExtender(
|
||||||
|
NewLogsExtender(NewBrowser(gvr), p.logOptions),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func TestPodNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, po.Init(makeCtx()))
|
assert.Nil(t, po.Init(makeCtx()))
|
||||||
assert.Equal(t, "Pods", po.Name())
|
assert.Equal(t, "Pods", po.Name())
|
||||||
assert.Equal(t, 27, len(po.Hints()))
|
assert.Equal(t, 28, len(po.Hints()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ type ReplicaSet struct {
|
||||||
// NewReplicaSet returns a new viewer.
|
// NewReplicaSet returns a new viewer.
|
||||||
func NewReplicaSet(gvr client.GVR) ResourceViewer {
|
func NewReplicaSet(gvr client.GVR) ResourceViewer {
|
||||||
r := ReplicaSet{
|
r := ReplicaSet{
|
||||||
ResourceViewer: NewVulnerabilityExtender(NewBrowser(gvr)),
|
ResourceViewer: NewOwnerExtender(
|
||||||
|
NewVulnerabilityExtender(
|
||||||
|
NewBrowser(gvr),
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
r.AddBindKeysFn(r.bindKeys)
|
r.AddBindKeysFn(r.bindKeys)
|
||||||
r.GetTable().SetEnterFn(r.showPods)
|
r.GetTable().SetEnterFn(r.showPods)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue