diff --git a/README.md b/README.md index 9189565d..24e8727f 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ K9s uses aliases to navigate most K8s resources. | `d`,`v`, `e`, `l`,... | Key mapping to describe, view, edit, view logs,... | `d` (describes a resource) | | `:`ctx`` | To view and switch to another Kubernetes context | `:`+`ctx`+`` | | `:`ns`` | To view and switch to another Kubernetes namespace | `:`+`ns`+`` | +| `:screendump`, `:sd` | To view all saved resources | | | `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | | | `Ctrl-k` | To delete a resource (no confirmation dialog) | | | `:q`, `Ctrl-c` | To bail out of K9s | | diff --git a/change_logs/release_0.10.10 copy.md b/change_logs/release_0.11.1.md similarity index 80% rename from change_logs/release_0.10.10 copy.md rename to change_logs/release_0.11.1.md index 6f69a4ee..29afdd2b 100644 --- a/change_logs/release_0.10.10 copy.md +++ b/change_logs/release_0.11.1.md @@ -1,6 +1,6 @@ -# Release v0.10.10 +# Release v0.11.1 ## Notes @@ -10,15 +10,15 @@ Also if you dig this tool, please make some noise on social! [@kitesurfer](https --- -## Change Logs + -Maintenance release! +Maintenance Release! --- ## Resolved Bugs/Features -* [Issue #463](https://github.com/derailed/k9s/issues/463) +* [Issue #466](https://github.com/derailed/k9s/issues/466) --- diff --git a/internal/client/gvr.go b/internal/client/gvr.go index eaa278d0..dbfac36f 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -88,6 +88,14 @@ func (g GVR) AsGVR() schema.GroupVersionResource { } } +// AsGR returns a a full schema representation. +func (g GVR) AsGR() *schema.GroupResource { + return &schema.GroupResource{ + Group: g.ToG(), + Resource: g.ToR(), + } +} + // ToV returns the resource version. func (g GVR) ToV() string { return g.v diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 4739fc88..96cffd78 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -21,27 +21,9 @@ type Generic struct { NonResource } -// Describe describes a resource. -func (g *Generic) Describe(path string) (string, error) { - return Describe(g.Client(), g.gvr, path) -} - -// ToYAML returns a resource yaml. -func (g *Generic) ToYAML(path string) (string, error) { - o, err := g.Get(context.Background(), path) - if err != nil { - return "", err - } - - raw, err := ToYAML(o) - if err != nil { - return "", fmt.Errorf("unable to marshal resource %s", err) - } - return raw, nil -} - // List returns a collection of resources. 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") @@ -85,6 +67,25 @@ func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) return req.Namespace(ns).Get(n, opts) } +// Describe describes a resource. +func (g *Generic) Describe(path string) (string, error) { + return Describe(g.Client(), g.gvr, path) +} + +// ToYAML returns a resource yaml. +func (g *Generic) ToYAML(path string) (string, error) { + o, err := g.Get(context.Background(), path) + if err != nil { + return "", err + } + + raw, err := ToYAML(o) + if err != nil { + return "", fmt.Errorf("unable to marshal resource %s", err) + } + return raw, nil +} + // Delete deletes a resource. func (g *Generic) Delete(path string, cascade, force bool) error { ns, n := client.Namespaced(path) diff --git a/internal/dao/table.go b/internal/dao/table.go new file mode 100644 index 00000000..4a7eb089 --- /dev/null +++ b/internal/dao/table.go @@ -0,0 +1,73 @@ +package dao + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" +) + +// Table retrieves K8s resources as tabular data. +type Table struct { + Generic +} + +// List all Resources in a given namespace. +func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { + a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) + _, codec := t.codec() + + c, err := t.getClient() + if err != nil { + return nil, err + } + + o, err := c.Get(). + SetHeader("Accept", a). + Namespace(ns). + Resource(t.gvr.ToR()). + VersionedParams(&metav1beta1.TableOptions{}, codec). + Do().Get() + if err != nil { + return nil, err + } + + return []runtime.Object{o}, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +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() + crConfig.GroupVersion = &gv + crConfig.APIPath = "/apis" + if len(t.gvr.ToG()) == 0 { + crConfig.APIPath = "/api" + } + codec, _ := t.codec() + crConfig.NegotiatedSerializer = codec.WithoutConversion() + + crRestClient, err := rest.RESTClientFor(crConfig) + if err != nil { + return nil, err + } + return crRestClient, nil +} + +func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { + scheme := runtime.NewScheme() + gv := t.gvr.AsGV() + metav1.AddToGroupVersion(scheme, gv) + scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + + return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) +} diff --git a/internal/model/table.go b/internal/model/table.go index 16e40018..8a493916 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" ) @@ -213,9 +214,21 @@ func (t *Table) reconcile(ctx context.Context) error { } log.Debug().Msgf("LIST returned %d rows", len(oo)) - rows := make(render.Rows, len(oo)) - if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil { - return err + var rows render.Rows + if _, ok := meta.Renderer.(*render.Generic); ok { + table, ok := oo[0].(*metav1beta1.Table) + if !ok { + return fmt.Errorf("expecting a meta table but got %T", oo[0]) + } + rows = make(render.Rows, len(table.Rows)) + if err := tableHydrate(t.namespace, table, rows, meta.Renderer); err != nil { + return err + } + } else { + rows = make(render.Rows, len(oo)) + if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil { + return err + } } t.data.Mutex.Lock() @@ -248,7 +261,7 @@ func (t *Table) resourceMeta() ResourceMeta { if !ok { log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr) meta = ResourceMeta{ - DAO: &dao.Generic{}, + DAO: &dao.Table{}, Renderer: &render.Generic{}, } } @@ -282,3 +295,18 @@ func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error return nil } + +func tableHydrate(ns string, table *metav1beta1.Table, rr render.Rows, re Renderer) error { + gr, ok := re.(*render.Generic) + if !ok { + return fmt.Errorf("expecting generic renderer but got %T", re) + } + gr.SetTable(table) + for i, row := range table.Rows { + if err := gr.Render(row, ns, &rr[i]); err != nil { + return err + } + } + + return nil +} diff --git a/internal/render/cm.go b/internal/render/cm.go deleted file mode 100644 index 1eb51aaa..00000000 --- a/internal/render/cm.go +++ /dev/null @@ -1,111 +0,0 @@ -package render - -import ( - "fmt" - "strconv" - "time" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/tview" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// ConfigMap renders a K8s ConfigMap to screen. -type ConfigMap struct{} - -// ColorerFunc colors a resource row. -func (ConfigMap) ColorerFunc() ColorerFunc { - return DefaultColorer -} - -// Header returns a header row. -func (ConfigMap) Header(ns string) HeaderRow { - var h HeaderRow - if client.IsAllNamespaces(ns) { - h = append(h, Header{Name: "NAMESPACE"}) - } - - return append(h, - Header{Name: "NAME"}, - Header{Name: "DATA", Align: tview.AlignRight}, - Header{Name: "AGE", Decorator: AgeDecorator}, - ) -} - -// Render renders a K8s resource to screen. -// BOZO!! 44allocs down to 5allocs avoiding marshal?? -func (c ConfigMap) Render(o interface{}, ns string, r *Row) error { - raw, ok := o.(*unstructured.Unstructured) - if !ok { - return fmt.Errorf("Expected ConfigMap, but got %T", o) - } - - meta, ok := raw.Object["metadata"].(map[string]interface{}) - if !ok { - return fmt.Errorf("No meta") - } - - n, nss := extractMetaField(meta, "name"), extractMetaField(meta, "namespace") - r.ID = FQN(nss, n) - r.Fields = make(Fields, 0, len(c.Header(ns))) - if client.IsAllNamespaces(ns) { - r.Fields = append(r.Fields, nss) - } - - var size int - data, ok := raw.Object["data"] - if ok { - d, ok := data.(map[string]interface{}) - if !ok { - return fmt.Errorf("expecting map but got %T", raw.Object["data"]) - } - size = len(d) - } - t, err := extractMetaTime(meta) - if err != nil { - return err - } - r.Fields = append(r.Fields, - n, - strconv.Itoa(size), - toAge(t), - ) - - // var cm v1.ConfigMap - // err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm) - // if err != nil { - // return err - // } - - // r.ID = MetaFQN(cm.ObjectMeta) - // r.Fields = make(Fields, 0, len(c.Header(ns))) - // if client.IsAllNamespaces(ns) { - // r.Fields = append(r.Fields, cm.Namespace) - // } - // r.Fields = append(r.Fields, - // cm.Name, - // strconv.Itoa(len(cm.Data)), - // toAge(cm.ObjectMeta.CreationTimestamp), - // ) - - return nil -} - -func extractMetaTime(m map[string]interface{}) (metav1.Time, error) { - f, ok := m["creationTimestamp"] - if !ok { - return metav1.Time{}, fmt.Errorf("failed to extract time from meta") - } - - t, ok := f.(string) - if !ok { - return metav1.Time{}, fmt.Errorf("failed to extract time from field") - } - - ti, err := time.Parse(time.RFC3339, t) - if err != nil { - return metav1.Time{}, err - } - return metav1.Time{Time: ti}, nil -} diff --git a/internal/render/cm_test.go b/internal/render/cm_test.go deleted file mode 100644 index 837a5c46..00000000 --- a/internal/render/cm_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package render_test - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestCmRender(t *testing.T) { - c := render.ConfigMap{} - r := render.NewRow(4) - - assert.Nil(t, c.Render(load(t, "cm"), "", &r)) - assert.Equal(t, "default/blee", r.ID) - assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3]) -} - -func BenchmarkCmRender(b *testing.B) { - c := render.ConfigMap{} - r := render.NewRow(4) - o := load(b, "cm") - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = c.Render(o, "", &r) - } -} - -// Helpers... - -func load(t assert.TestingT, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("assets/%s.json", n)) - assert.Nil(t, err) - - var o unstructured.Unstructured - err = json.Unmarshal(raw, &o) - assert.Nil(t, err) - - return &o -} diff --git a/internal/render/generic.go b/internal/render/generic.go index f26725b6..dddcb72a 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -55,7 +55,7 @@ func (g *Generic) Header(ns string) HeaderRow { // Render renders a K8s resource to screen. func (g *Generic) Render(o interface{}, ns string, r *Row) error { - row, ok := o.(*metav1beta1.TableRow) + row, ok := o.(metav1beta1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) } diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index d6378086..e5f1187e 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -85,7 +85,7 @@ func TestGenericRender(t *testing.T) { re.SetTable(u.table) assert.Equal(t, u.eHeader, re.Header(u.ns)) - assert.Nil(t, re.Render(&u.table.Rows[0], u.ns, &r)) + assert.Nil(t, re.Render(u.table.Rows[0], u.ns, &r)) assert.Equal(t, u.eID, r.ID) assert.Equal(t, u.eFields, r.Fields) }) diff --git a/internal/render/render_test.go b/internal/render/render_test.go new file mode 100644 index 00000000..dd758af1 --- /dev/null +++ b/internal/render/render_test.go @@ -0,0 +1,23 @@ +package render_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Helpers... + +func load(t assert.TestingT, n string) *unstructured.Unstructured { + raw, err := ioutil.ReadFile(fmt.Sprintf("assets/%s.json", n)) + assert.Nil(t, err) + + var o unstructured.Unstructured + err = json.Unmarshal(raw, &o) + assert.Nil(t, err) + + return &o +} diff --git a/internal/render/secret.go b/internal/render/secret.go deleted file mode 100644 index 4f1e2116..00000000 --- a/internal/render/secret.go +++ /dev/null @@ -1,62 +0,0 @@ -package render - -import ( - "fmt" - "strconv" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/tview" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// Secret renders a K8s Secret to screen. -type Secret struct{} - -// ColorerFunc colors a resource row. -func (Secret) ColorerFunc() ColorerFunc { - return DefaultColorer -} - -// Header returns a header row. -func (Secret) Header(ns string) HeaderRow { - var h HeaderRow - if client.IsAllNamespaces(ns) { - h = append(h, Header{Name: "NAMESPACE"}) - } - - return append(h, - Header{Name: "NAME"}, - Header{Name: "TYPE"}, - Header{Name: "DATA", Align: tview.AlignRight}, - Header{Name: "AGE", Decorator: AgeDecorator}, - ) -} - -// Render renders a K8s resource to screen. -func (s Secret) Render(o interface{}, ns string, r *Row) error { - raw, ok := o.(*unstructured.Unstructured) - if !ok { - return fmt.Errorf("Expected Secret, but got %T", o) - } - var sec v1.Secret - err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec) - if err != nil { - return err - } - - r.ID = MetaFQN(sec.ObjectMeta) - r.Fields = make(Fields, 0, len(s.Header(ns))) - if client.IsAllNamespaces(ns) { - r.Fields = append(r.Fields, sec.Namespace) - } - r.Fields = append(r.Fields, - sec.Name, - string(sec.Type), - strconv.Itoa(len(sec.Data)), - toAge(sec.ObjectMeta.CreationTimestamp), - ) - - return nil -} diff --git a/internal/render/secret_test.go b/internal/render/secret_test.go deleted file mode 100644 index e9ea35f8..00000000 --- a/internal/render/secret_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package render_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) - -func TestSecRender(t *testing.T) { - c := render.Secret{} - r := render.NewRow(4) - - c.Render(load(t, "sec"), "", &r) - assert.Equal(t, "default/s1", r.ID) - assert.Equal(t, render.Fields{"default", "s1", "Opaque", "2"}, r.Fields[:4]) -}