From c7e47d7be43e092c2dc765e1b257ab8f0491b63e Mon Sep 17 00:00:00 2001 From: gitolicious <26963495+gitolicious@users.noreply.github.com> Date: Mon, 1 Jan 2024 22:31:53 +0100 Subject: [PATCH] feat: show owner (#1747) --- internal/view/help_test.go | 2 +- internal/view/owner_extender.go | 133 ++++++++++++++++++++++++++++++++ internal/view/pod.go | 6 +- internal/view/pod_test.go | 2 +- internal/view/rs.go | 6 +- 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 internal/view/owner_extender.go diff --git a/internal/view/help_test.go b/internal/view/help_test.go index 818002ae72..0cc59870a2 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -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, 6, v.GetColumnCount()) assert.Equal(t, "", strings.TrimSpace(v.GetCell(1, 0).Text)) assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text)) diff --git a/internal/view/owner_extender.go b/internal/view/owner_extender.go new file mode 100644 index 0000000000..9a50731ff4 --- /dev/null +++ b/internal/view/owner_extender.go @@ -0,0 +1,133 @@ +package view + +import ( + "errors" + "fmt" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tcell/v2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// OwnerExtender adds log actions to a given viewer. +type OwnerExtender struct { + ResourceViewer +} + +// NewOwnerExtender returns a new extender. +func NewOwnerExtender(v ResourceViewer) ResourceViewer { + o := OwnerExtender{ + ResourceViewer: v, + } + o.AddBindKeysFn(o.bindKeys) + + return &o +} + +// BindKeys injects new menu actions. +func (o *OwnerExtender) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyO: ui.NewKeyAction("Show Owner", o.ownerCmd(false), true), + }) +} + +func (o *OwnerExtender) ownerCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + path := o.GetTable().GetSelectedItem() + if path == "" { + return nil + } + if !isResourcePath(path) { + path = o.GetTable().Path + } + o.showOwner(o.GetTable().GVR(), path, prev, evt) + + return nil + } +} + +func (o *OwnerExtender) showOwner(gvr client.GVR, path string, prev bool, evt *tcell.EventKey) { + var ownerReferences []v1.OwnerReference + + r, err := o.App().factory.Get(gvr.String(), path, true, labels.Everything()) + if err != nil { + o.App().Flash().Err(err) + return + } + + u, ok := r.(*unstructured.Unstructured) + if !ok { + o.App().Flash().Err(errors.New("unable to parse resource")) + return + } + + ownerReferences, err = o.extractOwnerReference(u) + if err != nil { + o.App().Flash().Err(err) + return + } + + var owner model.Component + for _, ownerReference := range ownerReferences { + ownerPath := u.GetNamespace() + "/" + ownerReference.Name + + switch ownerReference.Kind { + case "ReplicaSet": + rs := NewReplicaSet(client.NewGVR(ownerReference.APIVersion + "/replicasets")) + rs.SetInstance(ownerPath) + owner = rs + case "DaemonSet": + ds := NewDaemonSet(client.NewGVR(ownerReference.APIVersion + "/daemonsets")) + ds.SetInstance(ownerPath) + owner = ds + case "Deployment": + d := NewDeploy(client.NewGVR(ownerReference.APIVersion + "/deployments")) + d.SetInstance(ownerPath) + owner = d + } + } + + if owner == nil { + o.App().Flash().Err(errors.New(fmt.Sprintf("unsupported owner kind"))) + return + } + + if err := o.App().inject(owner, false); err != nil { + o.App().Flash().Err(err) + } + + return +} + +// extractOwnerReference extracts the OwnerReferences from an unstructured object +func (o *OwnerExtender) extractOwnerReference(obj *unstructured.Unstructured) ([]v1.OwnerReference, error) { + ownerRef, found, err := unstructured.NestedSlice(obj.Object, "metadata", "ownerReferences") + if err != nil { + return nil, err + } + if !found { + return nil, nil + } + + var ownerReferences []v1.OwnerReference + for _, ref := range ownerRef { + ownerReferenceMap, ok := ref.(map[string]interface{}) + if !ok { + return nil, errors.New("could not extract ownerReference") + } + + ownerReference := v1.OwnerReference{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(ownerReferenceMap, &ownerReference) + if err != nil { + return nil, err + } + + ownerReferences = append(ownerReferences, ownerReference) + } + + return ownerReferences, nil +} diff --git a/internal/view/pod.go b/internal/view/pod.go index 1ef29bab80..cb8763e3b1 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -47,7 +47,11 @@ func NewPod(gvr client.GVR) ResourceViewer { p.ResourceViewer = NewPortForwardExtender( NewVulnerabilityExtender( NewImageExtender( - NewLogsExtender(NewBrowser(gvr), p.logOptions), + NewOwnerExtender( + NewLogsExtender( + NewBrowser(gvr), + p.logOptions), + ), ), ), ) diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index bbbf0378b9..23bdebaf74 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -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... diff --git a/internal/view/rs.go b/internal/view/rs.go index 3650b8ff15..7f18525a87 100644 --- a/internal/view/rs.go +++ b/internal/view/rs.go @@ -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: NewVulnerabilityExtender( + NewOwnerExtender( + NewBrowser(gvr), + ), + ), } r.AddBindKeysFn(r.bindKeys) r.GetTable().SetEnterFn(r.showPods)