derailed 2020-01-02 18:15:38 -07:00
parent a4e52e41c7
commit 48869f9f51
17 changed files with 375 additions and 119 deletions

View File

@ -569,12 +569,12 @@ to make this project a reality!
## Meet The Core Team!
* [Gustavo Silva Paiva](https://github.com/paivagustavo)
* <img src="assets/mail.png" width="16" height="auto"/> guustavo.paiva@gmail.com
* <img src="assets/twitter.png" width="16" height="auto"/> [@paivagustavodev](https://twitter.com/paivagustavodev)
* [Fernand Galiana](https://github.com/derailed)
* <img src="assets/mail.png" width="16" height="auto"/> fernand@imhotep.io
* <img src="assets/twitter.png" width="16" height="auto"/> [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
* [Gustavo Silva Paiva](https://github.com/paivagustavo)
* <img src="assets/mail.png" width="16" height="auto"/> guustavo.paiva@gmail.com
* <img src="assets/twitter.png" width="16" height="auto"/> [@paivagustavodev](https://twitter.com/paivagustavodev)
---

View File

@ -0,0 +1,25 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.10.10
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
---
## Change Logs
Maintenance release!
---
## Resolved Bugs/Features
* [Issue #463](https://github.com/derailed/k9s/issues/463)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -6,6 +6,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
@ -23,6 +24,20 @@ type Generic struct {
table *metav1beta1.Table
}
// List returns a collection of node resources.
func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) {
var opts metav1.GetOptions
ns, n := client.Namespaced(path)
gvr := client.NewGVR(g.gvr)
req := g.factory.Client().DynDialOrDie().Resource(gvr.AsGVR())
if ns == "" {
return req.Get(n, opts)
}
return req.Namespace(ns).Get(n, opts)
}
// List returns a collection of node resources.
func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
// Ensures the factory is tracking this resource
@ -39,13 +54,19 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
return nil, err
}
// BOZO!! Need to know if gvr is namespaced or not
ns := g.namespace
if g.namespace == render.ClusterScope {
ns = render.AllNamespaces
}
log.Debug().Msgf("GENERIC LIST %q:%q", g.namespace, g.gvr)
o, err := c.Get().
SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
Namespace(g.namespace).
Resource(gvr.ToR()).
VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get()
Namespace(ns).
Do().
Get()
if err != nil {
return nil, err
}

View File

@ -21,6 +21,11 @@ func (r *Resource) Init(ns, gvr string, f dao.Factory) {
r.namespace, r.gvr, r.factory = ns, gvr, f
}
// Get returns a resource instance if found, else an error.
func (r *Resource) Get(ctx context.Context, path string) (runtime.Object, error) {
return r.factory.Get(r.gvr, path, true, labels.Everything())
}
// List returns a collection of nodes.
func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) {
strLabel, ok := ctx.Value(internal.KeyLabels).(string)

View File

@ -47,6 +47,18 @@ func (t *Table) Watch(ctx context.Context) {
go t.updater(ctx)
}
// Get returns a resource instance if found, else an error.
func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
meta := t.resourceMeta()
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.Model.Init(t.namespace, t.gvr, factory)
return meta.Model.Get(ctx, path)
}
// Refresh update the model now.
func (t *Table) Refresh(ctx context.Context) {
t.refresh(ctx)
@ -158,11 +170,7 @@ func (t *Table) list(ctx context.Context, l Lister) ([]runtime.Object, error) {
return l.List(ctx)
}
func (t *Table) reconcile(ctx context.Context) error {
defer func(t time.Time) {
log.Debug().Msgf("RECONCILE elapsed %v", time.Since(t))
}(time.Now())
func (t *Table) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr]
if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
@ -175,6 +183,15 @@ func (t *Table) reconcile(ctx context.Context) error {
meta.Model = &Resource{}
}
return meta
}
func (t *Table) reconcile(ctx context.Context) error {
defer func(t time.Time) {
log.Debug().Msgf("RECONCILE elapsed %v", time.Since(t))
}(time.Now())
meta := t.resourceMeta()
oo, err := t.list(ctx, meta.Model)
if err != nil {
return err

View File

@ -62,6 +62,9 @@ type Lister interface {
// List returns a collection of resources.
List(context.Context) ([]runtime.Object, error)
// Get returns a resource instance.
Get(ctx context.Context, path string) (runtime.Object, error)
// Hydrate converts resource rows into tabular data.
Hydrate(oo []runtime.Object, rr render.Rows, r Renderer) error
}

View File

@ -9,9 +9,13 @@ import (
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
)
const ageTableCol = "Age"
// Generic renders a generic resource to screen.
type Generic struct {
table *metav1beta1.Table
ageIndex int
}
// SetTable sets the tabular resource.
@ -34,9 +38,16 @@ func (g *Generic) Header(ns string) HeaderRow {
if ns == "" {
h = append(h, Header{Name: "NAMESPACE"})
}
for _, c := range g.table.ColumnDefinitions {
for i, c := range g.table.ColumnDefinitions {
if c.Name == ageTableCol {
g.ageIndex = i
continue
}
h = append(h, Header{Name: strings.ToUpper(c.Name)})
}
if g.ageIndex > 0 {
h = append(h, Header{Name: "AGE"})
}
return h
}
@ -48,24 +59,35 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
return fmt.Errorf("expecting a TableRow but got %T", o)
}
r.ID, ok = row.Cells[0].(string)
var nns = AllNamespaces
if ns != ClusterScope {
var err error
nns, err = extractNamespace(row.Object.Raw)
if err != nil {
return err
}
}
n, ok := row.Cells[0].(string)
if !ok {
return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0])
}
rns, err := extractNamespace(row.Object.Raw)
if err != nil {
return err
}
r.ID = FQN(rns, r.ID)
r.ID = FQN(nns, n)
r.Fields = make(Fields, 0, len(g.Header(ns)))
if isAllNamespace(ns) {
r.Fields = append(r.Fields, rns)
r.Fields = append(r.Fields, nns)
}
for _, c := range row.Cells {
var ageCell interface{}
for i, c := range row.Cells {
if g.ageIndex > 0 && i == g.ageIndex {
ageCell = c
continue
}
r.Fields = append(r.Fields, fmt.Sprintf("%v", c))
}
if ageCell != nil {
r.Fields = append(r.Fields, fmt.Sprintf("%v", ageCell))
}
return nil
}

View File

@ -10,24 +10,83 @@ import (
)
func TestGenericRender(t *testing.T) {
var g render.Generic
uu := map[string]struct {
ns string
table *metav1beta1.Table
eID string
eFields render.Fields
eHeader render.HeaderRow
}{
"specific_ns": {
ns: "blee",
table: makeNSGeneric(),
eID: "ns1/c1",
eFields: render.Fields{"c1", "c2", "c3"},
eHeader: render.HeaderRow{
render.Header{Name: "A"},
render.Header{Name: "B"},
render.Header{Name: "C"},
},
},
"all_ns": {
ns: "",
table: makeAllNSGeneric(),
eID: "ns1/c1",
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
eHeader: render.HeaderRow{
render.Header{Name: "NAMESPACE"},
render.Header{Name: "A"},
render.Header{Name: "B"},
render.Header{Name: "C"},
},
},
"cluster": {
ns: "-",
table: makeClusterGeneric(),
eID: "c1",
eFields: render.Fields{"c1", "c2", "c3"},
eHeader: render.HeaderRow{
render.Header{Name: "A"},
render.Header{Name: "B"},
render.Header{Name: "C"},
},
},
"age": {
ns: "-",
table: makeAgeGeneric(),
eID: "c1",
eFields: render.Fields{"c1", "c2", "Age"},
eHeader: render.HeaderRow{
render.Header{Name: "A"},
render.Header{Name: "C"},
render.Header{Name: "AGE"},
},
},
}
var r render.Row
row := makeGeneric().Rows[0]
assert.Nil(t, g.Render(&row, "blee", &r))
assert.Equal(t, "blee/a", r.ID)
assert.Equal(t, render.Fields{"a", "b", "c"}, r.Fields)
var re render.Generic
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
var r render.Row
re.SetTable(u.table)
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)
assert.Equal(t, u.eHeader, re.Header(u.ns))
})
}
}
// ----------------------------------------------------------------------------
// Helpers...
func makeGeneric() *metav1beta1.Table {
func makeNSGeneric() *metav1beta1.Table {
return &metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "A"},
{Name: "B"},
{Name: "C"},
{Name: "a"},
{Name: "b"},
{Name: "c"},
},
Rows: []metav1beta1.TableRow{
{
@ -36,14 +95,96 @@ func makeGeneric() *metav1beta1.Table {
"kind": "fred",
"apiVersion": "v1",
"metadata": {
"namespace": "blee",
"namespace": "ns1",
"name": "fred"
}}`),
},
Cells: []interface{}{
"a",
"b",
"c",
"c1",
"c2",
"c3",
},
},
},
}
}
func makeAllNSGeneric() *metav1beta1.Table {
return &metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "a"},
{Name: "b"},
{Name: "c"},
},
Rows: []metav1beta1.TableRow{
{
Object: runtime.RawExtension{
Raw: []byte(`{
"kind": "fred",
"apiVersion": "v1",
"metadata": {
"namespace": "ns1",
"name": "fred"
}}`),
},
Cells: []interface{}{
"c1",
"c2",
"c3",
},
},
},
}
}
func makeClusterGeneric() *metav1beta1.Table {
return &metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "a"},
{Name: "b"},
{Name: "c"},
},
Rows: []metav1beta1.TableRow{
{
Object: runtime.RawExtension{
Raw: []byte(`{
"kind": "fred",
"apiVersion": "v1",
"metadata": {
"name": "fred"
}}`),
},
Cells: []interface{}{
"c1",
"c2",
"c3",
},
},
},
}
}
func makeAgeGeneric() *metav1beta1.Table {
return &metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "a"},
{Name: "Age"},
{Name: "c"},
},
Rows: []metav1beta1.TableRow{
{
Object: runtime.RawExtension{
Raw: []byte(`{
"kind": "fred",
"apiVersion": "v1",
"metadata": {
"name": "fred"
}}`),
},
Cells: []interface{}{
"c1",
"Age",
"c2",
},
},
},

View File

@ -1,50 +1,10 @@
package ui
import (
"context"
"time"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
// Namespaceable represents a namespaceable model.
type Namespaceable interface {
// ClusterWide returns true if the model represents resource in all namespaces.
ClusterWide() bool
// GetNamespace returns the model namespace.
GetNamespace() string
// SetNamespace changes the model namespace.
SetNamespace(string)
// InNamespace check if current namespace matches models.
InNamespace(string) bool
}
// Tabular represents a tabular model.
type Tabular interface {
Namespaceable
// Empty returns true if model has no data.
Empty() bool
// Peek returns current model data.
Peek() render.TableData
// Watch watches a given resource for changes.
Watch(context.Context)
// SetRefreshRate sets the model watch loop rate.
SetRefreshRate(time.Duration)
// AddListener registers a model listener.
AddListener(model.TableListener)
}
// SelectTable represents a table with selections.
type SelectTable struct {
*tview.Table

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestTableNew(t *testing.T) {
@ -66,8 +67,11 @@ func (t *testModel) GetNamespace() string { return "blee" }
func (t *testModel) SetNamespace(string) {}
func (t *testModel) AddListener(model.TableListener) {}
func (t *testModel) Watch(context.Context) {}
func (t *testModel) InNamespace(string) bool { return true }
func (t *testModel) SetRefreshRate(time.Duration) {}
func (t *testModel) Get(ctx context.Context, path string) (runtime.Object, error) {
return nil, nil
}
func (t *testModel) InNamespace(string) bool { return true }
func (t *testModel) SetRefreshRate(time.Duration) {}
func makeTableData() render.TableData {
t := render.NewTableData()

View File

@ -1,6 +1,13 @@
package ui
import "github.com/derailed/k9s/internal/render"
import (
"context"
"time"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"k8s.io/apimachinery/pkg/runtime"
)
type (
// SortFn represent a function that can sort columnar data.
@ -13,3 +20,41 @@ type (
asc bool
}
)
// Namespaceable represents a namespaceable model.
type Namespaceable interface {
// ClusterWide returns true if the model represents resource in all namespaces.
ClusterWide() bool
// GetNamespace returns the model namespace.
GetNamespace() string
// SetNamespace changes the model namespace.
SetNamespace(string)
// InNamespace check if current namespace matches models.
InNamespace(string) bool
}
// Tabular represents a tabular model.
type Tabular interface {
Namespaceable
// Empty returns true if model has no data.
Empty() bool
// Peek returns current model data.
Peek() render.TableData
// Watch watches a given resource for changes.
Watch(context.Context)
// SetRefreshRate sets the model watch loop rate.
SetRefreshRate(time.Duration)
// AddListener registers a model listener.
AddListener(model.TableListener)
// Get returns a resource instance.
Get(ctx context.Context, path string) (runtime.Object, error)
}

View File

@ -15,6 +15,7 @@ import (
"github.com/gdamore/tcell"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func TestAliasNew(t *testing.T) {
@ -105,8 +106,11 @@ func (t *testModel) GetNamespace() string { return "blee" }
func (t *testModel) SetNamespace(string) {}
func (t *testModel) AddListener(model.TableListener) {}
func (t *testModel) Watch(context.Context) {}
func (t *testModel) InNamespace(string) bool { return true }
func (t *testModel) SetRefreshRate(time.Duration) {}
func (t *testModel) Get(ctx context.Context, path string) (runtime.Object, error) {
return nil, nil
}
func (t *testModel) InNamespace(string) bool { return true }
func (t *testModel) SetRefreshRate(time.Duration) {}
func makeTableData() render.TableData {
return render.TableData{

View File

@ -151,6 +151,33 @@ func (b *Browser) TableLoadFailed(err error) {
// ----------------------------------------------------------------------------
// Actions...
func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
path := b.GetSelectedItem()
if path == "" {
return evt
}
ctx := b.defaultContext()
o, err := b.GetModel().Get(ctx, path)
if err != nil {
b.App().Flash().Errf("unable to get resource %q -- %s", b.gvr, err)
return nil
}
raw, err := toYAML(o)
if err != nil {
b.App().Flash().Errf("unable to marshal resource %s", err)
return nil
}
details := NewDetails(b.app, "YAML", path).Update(raw)
if err := b.App().inject(details); err != nil {
b.App().Flash().Err(err)
}
return nil
}
func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !b.SearchBuff().InCmdMode() {
b.SearchBuff().Reset()

View File

@ -10,7 +10,6 @@ import (
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/labels"
)
// Table represents a table viewer.
@ -128,32 +127,6 @@ func (t *Table) bindKeys() {
})
}
func (t *Table) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
path := t.GetSelectedItem()
if path == "" {
return evt
}
o, err := t.app.factory.Get(t.GVR(), path, true, labels.Everything())
if err != nil {
t.app.Flash().Errf("Unable to get resource %q -- %s", t.gvr, err)
return nil
}
raw, err := toYAML(o)
if err != nil {
t.app.Flash().Errf("Unable to marshal resource %s", err)
return nil
}
details := NewDetails(t.app, "YAML", path).Update(raw)
if err := t.app.inject(details); err != nil {
t.App().Flash().Err(err)
}
return nil
}
func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
path := t.GetSelectedItem()
if path == "" {

View File

@ -16,6 +16,7 @@ import (
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func TestTableSave(t *testing.T) {
@ -97,8 +98,11 @@ func (t *testTableModel) GetNamespace() string { return "blee" }
func (t *testTableModel) SetNamespace(string) {}
func (t *testTableModel) AddListener(model.TableListener) {}
func (t *testTableModel) Watch(context.Context) {}
func (t *testTableModel) InNamespace(string) bool { return true }
func (t *testTableModel) SetRefreshRate(time.Duration) {}
func (t *testTableModel) Get(ctx context.Context, path string) (runtime.Object, error) {
return nil, nil
}
func (t *testTableModel) InNamespace(string) bool { return true }
func (t *testTableModel) SetRefreshRate(time.Duration) {}
func makeTableData() render.TableData {
t := render.NewTableData()

View File

@ -62,35 +62,36 @@ func (f *Factory) Terminate() {
// List returns a resource collection.
func (f *Factory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
if ns == clusterScope {
ns = allNamespaces
}
log.Debug().Msgf("List %q:%q", ns, gvr)
inf, err := f.CanForResource(ns, gvr, []string{"list", "watch"})
if err != nil {
return nil, err
}
if ns == clusterScope {
ns = allNamespaces
}
if wait {
f.waitForCacheSync(ns)
}
return inf.Lister().ByNamespace(ns).List(sel)
}
// Get retrieves a given resource.
func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
ns, n := namespaced(path)
if ns == clusterScope {
ns = allNamespaces
}
log.Debug().Msgf("GET %q:%q::%q", ns, gvr, n)
inf, err := f.CanForResource(ns, gvr, []string{"get"})
if err != nil {
return nil, err
}
if ns == clusterScope {
ns = allNamespaces
}
if wait {
f.waitForCacheSync(ns)
}
return inf.Lister().ByNamespace(ns).Get(n)
}

View File

@ -39,7 +39,11 @@ func Dump(f *Factory) {
// Debug for debug.
func Debug(f *Factory, ns string, gvr string) {
log.Debug().Msgf("----------- DEBUG FACTORY (%s) -------------", gvr)
inf := f.factories[ns].ForResource(toGVR(gvr))
fac, ok := f.factories[ns]
if !ok {
return
}
inf := fac.ForResource(toGVR(gvr))
for i, k := range inf.Informer().GetStore().ListKeys() {
log.Debug().Msgf("%d -- %s", i, k)
}