Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow jumping to the owner of the resource #2700

Merged
merged 1 commit into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions internal/dao/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

const (
Expand Down Expand Up @@ -123,6 +124,20 @@ func (m *Meta) AllGVRs() client.GVRs {
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
func IsCRD(r metav1.APIResource) bool {
for _, c := range r.Categories {
Expand Down
30 changes: 30 additions & 0 deletions internal/ui/dialog/selection.go
Original file line number Diff line number Diff line change
@@ -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)
}
109 changes: 109 additions & 0 deletions internal/ui/modal_list.go
Original file line number Diff line number Diff line change
@@ -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
}
}
})
}
3 changes: 1 addition & 2 deletions internal/ui/pages.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package ui

import (
"fmt"

"github.com/derailed/k9s/internal/model"
"github.com/derailed/tview"
"github.com/rs/zerolog/log"
Expand All @@ -32,7 +31,7 @@ func NewPages() *Pages {
func (p *Pages) IsTopDialog() bool {
_, pa := p.GetFrontPage()
switch pa.(type) {
case *tview.ModalForm:
case *tview.ModalForm, *ModalList:
return true
default:
return false
Expand Down
2 changes: 1 addition & 1 deletion internal/view/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestHelp(t *testing.T) {
v := view.NewHelp(app)

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, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))
Expand Down
4 changes: 3 additions & 1 deletion internal/view/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ func NewJob(gvr client.GVR) ResourceViewer {
var j Job

j.ResourceViewer = NewVulnerabilityExtender(
NewLogsExtender(NewBrowser(gvr), j.logOptions),
NewOwnerExtender(
NewLogsExtender(NewBrowser(gvr), j.logOptions),
),
)
j.GetTable().SetEnterFn(j.showPods)
j.GetTable().SetSortCol("AGE", true)
Expand Down
118 changes: 118 additions & 0 deletions internal/view/owner_extender.go
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 5 additions & 3 deletions internal/view/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ type Pod struct {
func NewPod(gvr client.GVR) ResourceViewer {
var p Pod
p.ResourceViewer = NewPortForwardExtender(
NewVulnerabilityExtender(
NewImageExtender(
NewLogsExtender(NewBrowser(gvr), p.logOptions),
NewOwnerExtender(
NewVulnerabilityExtender(
NewImageExtender(
NewLogsExtender(NewBrowser(gvr), p.logOptions),
),
),
),
)
Expand Down
2 changes: 1 addition & 1 deletion internal/view/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestPodNew(t *testing.T) {

assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "Pods", po.Name())
assert.Equal(t, 27, len(po.Hints()))
assert.Equal(t, 28, len(po.Hints()))
}

// Helpers...
Expand Down
6 changes: 5 additions & 1 deletion internal/view/rs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ type ReplicaSet struct {
// NewReplicaSet returns a new viewer.
func NewReplicaSet(gvr client.GVR) ResourceViewer {
r := ReplicaSet{
ResourceViewer: NewVulnerabilityExtender(NewBrowser(gvr)),
ResourceViewer: NewOwnerExtender(
NewVulnerabilityExtender(
NewBrowser(gvr),
),
),
}
r.AddBindKeysFn(r.bindKeys)
r.GetTable().SetEnterFn(r.showPods)
Expand Down