fix #466
parent
92214ed3ee
commit
6b5da1091e
|
|
@ -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`<ENTER>` | To view and switch to another Kubernetes context | `:`+`ctx`+`<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-k` | To delete a resource (no confirmation dialog) | |
|
||||
| `:q`, `Ctrl-c` | To bail out of K9s | |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
* [Issue #463](https://github.com/derailed/k9s/issues/463)
|
||||
* [Issue #466](https://github.com/derailed/k9s/issues/466)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
Loading…
Reference in New Issue