derailed 2020-01-06 09:47:52 -07:00
parent 92214ed3ee
commit 6b5da1091e
13 changed files with 163 additions and 264 deletions

View File

@ -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) | | `d`,`v`, `e`, `l`,... | Key mapping to describe, view, edit, view logs,... | `d` (describes a resource) |
| `:`ctx`<ENTER>` | To view and switch to another Kubernetes context | `:`+`ctx`+`<ENTER>` | | `:`ctx`<ENTER>` | To view and switch to another Kubernetes context | `:`+`ctx`+`<ENTER>` |
| `:`ns`<ENTER>` | To view and switch to another Kubernetes namespace | `:`+`ns`+`<ENTER>` | | `:`ns`<ENTER>` | To view and switch to another Kubernetes namespace | `:`+`ns`+`<ENTER>` |
| `:screendump`, `:sd` | To view all saved resources | |
| `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | | | `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | |
| `Ctrl-k` | To delete a resource (no confirmation dialog) | | | `Ctrl-k` | To delete a resource (no confirmation dialog) | |
| `:q`, `Ctrl-c` | To bail out of K9s | | | `:q`, `Ctrl-c` | To bail out of K9s | |

View File

@ -1,6 +1,6 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/> <img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.10.10 # Release v0.11.1
## Notes ## Notes
@ -10,15 +10,15 @@ Also if you dig this tool, please make some noise on social! [@kitesurfer](https
--- ---
## Change Logs <img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_helm.png" align="center" width="300" height="auto"/>
Maintenance release! Maintenance Release!
--- ---
## Resolved Bugs/Features ## Resolved Bugs/Features
* [Issue #463](https://github.com/derailed/k9s/issues/463) * [Issue #466](https://github.com/derailed/k9s/issues/466)
--- ---

View File

@ -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. // ToV returns the resource version.
func (g GVR) ToV() string { func (g GVR) ToV() string {
return g.v return g.v

View File

@ -21,27 +21,9 @@ type Generic struct {
NonResource 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. // List returns a collection of resources.
func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) { 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) labelSel, ok := ctx.Value(internal.KeyLabels).(string)
if !ok { if !ok {
log.Warn().Msgf("No label selector found in context. Listing all resources") 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) 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. // Delete deletes a resource.
func (g *Generic) Delete(path string, cascade, force bool) error { func (g *Generic) Delete(path string, cascade, force bool) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)

73
internal/dao/table.go Normal file
View File

@ -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)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime" "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)) log.Debug().Msgf("LIST returned %d rows", len(oo))
rows := make(render.Rows, len(oo)) var rows render.Rows
if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil { if _, ok := meta.Renderer.(*render.Generic); ok {
return err 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() t.data.Mutex.Lock()
@ -248,7 +261,7 @@ func (t *Table) resourceMeta() ResourceMeta {
if !ok { if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr) log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
meta = ResourceMeta{ meta = ResourceMeta{
DAO: &dao.Generic{}, DAO: &dao.Table{},
Renderer: &render.Generic{}, Renderer: &render.Generic{},
} }
} }
@ -282,3 +295,18 @@ func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error
return nil 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -55,7 +55,7 @@ func (g *Generic) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (g *Generic) Render(o interface{}, ns string, r *Row) error { func (g *Generic) Render(o interface{}, ns string, r *Row) error {
row, ok := o.(*metav1beta1.TableRow) row, ok := o.(metav1beta1.TableRow)
if !ok { if !ok {
return fmt.Errorf("expecting a TableRow but got %T", o) return fmt.Errorf("expecting a TableRow but got %T", o)
} }

View File

@ -85,7 +85,7 @@ func TestGenericRender(t *testing.T) {
re.SetTable(u.table) re.SetTable(u.table)
assert.Equal(t, u.eHeader, re.Header(u.ns)) 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.eID, r.ID)
assert.Equal(t, u.eFields, r.Fields) assert.Equal(t, u.eFields, r.Fields)
}) })

View File

@ -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
}

View File

@ -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
}

View File

@ -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])
}