mine
derailed 2019-12-31 12:33:54 -07:00
parent ee01ae7242
commit 4c222f80ed
20 changed files with 486 additions and 109 deletions

View File

@ -0,0 +1,29 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.10.3
## 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!
Thank you all for kicking the tires on these new drops and in making sure we get back to nominal quickly. You guys ROCK!!
---
## Resolved Bugs/Features
* [Issue #455](https://github.com/derailed/k9s/issues/455)
* [Issue #454](https://github.com/derailed/k9s/issues/454)
* [Issue #453](https://github.com/derailed/k9s/issues/453)
---
<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

@ -0,0 +1,99 @@
{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {
"annotations": {
"helm.sh/resource-policy": "keep",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"annotations\":{\"helm.sh/resource-policy\":\"keep\"},\"labels\":{\"app\":\"istio-pilot\",\"chart\":\"istio\",\"heritage\":\"Tiller\",\"release\":\"istio\"},\"name\":\"destinationrules.networking.istio.io\"},\"spec\":{\"additionalPrinterColumns\":[{\"JSONPath\":\".spec.host\",\"description\":\"The name of a service from the service registry\",\"name\":\"Host\",\"type\":\"string\"},{\"JSONPath\":\".metadata.creationTimestamp\",\"description\":\"CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\\n\\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata\",\"name\":\"Age\",\"type\":\"date\"}],\"group\":\"networking.istio.io\",\"names\":{\"categories\":[\"istio-io\",\"networking-istio-io\"],\"kind\":\"DestinationRule\",\"listKind\":\"DestinationRuleList\",\"plural\":\"destinationrules\",\"shortNames\":[\"dr\"],\"singular\":\"destinationrule\"},\"scope\":\"Namespaced\",\"version\":\"v1alpha3\"}}\n"
},
"creationTimestamp": "2019-12-30T16:13:02Z",
"generation": 1,
"labels": {
"app": "istio-pilot",
"chart": "istio",
"heritage": "Tiller",
"release": "istio"
},
"name": "destinationrules.networking.istio.io",
"resourceVersion": "2773373",
"selfLink": "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/destinationrules.networking.istio.io",
"uid": "123a30f8-8fcf-44b5-84b7-35f8c7869828"
},
"spec": {
"conversion": {
"strategy": "None"
},
"group": "networking.istio.io",
"version": "v1alpha3",
"names": {
"categories": [
"istio-io",
"networking-istio-io"
],
"kind": "DestinationRule",
"listKind": "DestinationRuleList",
"plural": "destinationrules",
"shortNames": [
"dr"
],
"singular": "destinationrule"
},
"preserveUnknownFields": true,
"scope": "Namespaced",
"versions": [
{
"additionalPrinterColumns": [
{
"description": "The name of a service from the service registry",
"jsonPath": ".spec.host",
"name": "Host",
"type": "string"
},
{
"description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata",
"jsonPath": ".metadata.creationTimestamp",
"name": "Age",
"type": "date"
}
],
"name": "v1alpha3",
"served": true,
"storage": true
}
]
},
"status": {
"acceptedNames": {
"categories": [
"istio-io",
"networking-istio-io"
],
"kind": "DestinationRule",
"listKind": "DestinationRuleList",
"plural": "destinationrules",
"shortNames": [
"dr"
],
"singular": "destinationrule"
},
"conditions": [
{
"lastTransitionTime": "2019-12-30T16:13:02Z",
"message": "no conflicts found",
"reason": "NoConflicts",
"status": "True",
"type": "NamesAccepted"
},
{
"lastTransitionTime": "2019-12-30T16:13:02Z",
"message": "the initial names have been accepted",
"reason": "InitialNamesAccepted",
"status": "True",
"type": "Established"
}
],
"storedVersions": [
"v1alpha3"
]
}
}

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -27,6 +28,7 @@ var _ Scalable = &Deployment{}
// Scale a Deployment. // Scale a Deployment.
func (d *Deployment) Scale(path string, replicas int32) error { func (d *Deployment) Scale(path string, replicas int32) error {
log.Debug().Msgf("SCALING DEPLOYMENT!! %q:%d", path, replicas)
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{}) scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{})
if err != nil { if err != nil {

View File

@ -218,7 +218,7 @@ func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
crd, ok := o.(*unstructured.Unstructured) crd, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return m, append(errs, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o)) return m, append(errs, fmt.Errorf("Expected Unstructured, but got %T", o))
} }
var spec map[string]interface{} var spec map[string]interface{}
@ -254,6 +254,7 @@ func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, [
if m[n] == nil { if m[n] == nil {
return nil, errs return nil, errs
} }
s, ok := m[n].([]string) s, ok := m[n].([]string)
if ok { if ok {
return s, errs return s, errs
@ -268,10 +269,11 @@ func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, [
for i, name := range ii { for i, name := range ii {
ss[i], ok = name.(string) ss[i], ok = name.(string)
if !ok { if !ok {
return s, append(errs, fmt.Errorf("expecting string shortnames")) return ss, append(errs, fmt.Errorf("expecting string shortnames"))
} }
} }
return s, errs
return ss, errs
} }
func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) { func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) {

View File

@ -0,0 +1,100 @@
package dao
import (
"encoding/json"
"fmt"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestExtractMeta(t *testing.T) {
c := load(t, "dr")
m, ee := extractMeta(c)
assert.Equal(t, 0, len(ee))
assert.Equal(t, "destinationrules", m.Name)
assert.Equal(t, "destinationrule", m.SingularName)
assert.Equal(t, "DestinationRule", m.Kind)
assert.Equal(t, "networking.istio.io", m.Group)
assert.Equal(t, "v1alpha3", m.Version)
assert.Equal(t, true, m.Namespaced)
assert.Equal(t, []string{"dr"}, m.ShortNames)
var vv metav1.Verbs
assert.Equal(t, vv, m.Verbs)
}
func TestExtractSlice(t *testing.T) {
uu := map[string]struct {
m map[string]interface{}
n string
nn []string
ee []error
}{
"plain": {
m: map[string]interface{}{"shortNames": []string{"a", "b", "c"}},
n: "shortNames",
nn: []string{"a", "b", "c"},
},
"empty": {
m: map[string]interface{}{},
n: "shortNames",
},
}
var ee []error
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
ss, e := extractSlice(u.m, u.n, ee)
assert.Equal(t, u.ee, e)
assert.Equal(t, u.nn, ss)
})
}
}
func TestExtractString(t *testing.T) {
uu := map[string]struct {
m map[string]interface{}
n string
s string
ee []error
}{
"plain": {
m: map[string]interface{}{"blee": "fred"},
n: "blee",
s: "fred",
},
"missing": {
m: map[string]interface{}{},
n: "blee",
ee: []error{fmt.Errorf("failed to extract string blee")},
},
}
var ee []error
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
as, ae := extractStr(u.m, u.n, ee)
assert.Equal(t, u.ee, ae)
assert.Equal(t, u.s, as)
})
}
}
// Helpers...
func load(t *testing.T, 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

@ -13,7 +13,7 @@ import (
) )
const ( const (
refreshRate = 1 * time.Second refreshRate = 2 * time.Second
noDataCount = 2 noDataCount = 2
) )

View File

@ -117,19 +117,11 @@ func (rr RowEvents) Upsert(e RowEvent) RowEvents {
// Delete removes an element by id. // Delete removes an element by id.
func (rr RowEvents) Delete(id string) RowEvents { func (rr RowEvents) Delete(id string) RowEvents {
idx, ok := rr.FindIndex(id) victim, ok := rr.FindIndex(id)
if !ok { if !ok {
return rr return rr
} }
return append(rr[0:victim], rr[victim+1:]...)
if idx == 0 {
return rr[1:]
}
if idx == len(rr)-1 {
return rr[:len(rr)-1]
}
return append(rr[:idx], rr[idx+1:]...)
} }
// Clear delete all row events // Clear delete all row events

View File

@ -8,6 +8,58 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestRowEventsDelete(t *testing.T) {
uu := map[string]struct {
re render.RowEvents
id string
e render.RowEvents
}{
"first": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
id: "A",
e: render.RowEvents{
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
"middle": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
id: "B",
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
"last": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
id: "C",
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.re.Delete(u.id))
})
}
}
func TestSort(t *testing.T) { func TestSort(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
re render.RowEvents re render.RowEvents
@ -32,10 +84,10 @@ func TestSort(t *testing.T) {
} }
for k := range uu { for k := range uu {
uc := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
uc.re.Sort("", uc.col, uc.asc) u.re.Sort("", u.col, u.asc)
assert.Equal(t, uc.e, uc.re) assert.Equal(t, u.e, u.re)
}) })
} }
} }
@ -52,9 +104,9 @@ func TestDefaultColorer(t *testing.T) {
} }
for k := range uu { for k := range uu {
uc := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
assert.Equal(t, uc.e, render.DefaultColorer("", render.RowEvent{})) assert.Equal(t, u.e, render.DefaultColorer("", render.RowEvent{}))
}) })
} }
} }

View File

@ -1,6 +1,10 @@
package render package render
import "sync" import (
"sync"
"github.com/rs/zerolog/log"
)
// TableData tracks a K8s resource for tabular display. // TableData tracks a K8s resource for tabular display.
type TableData struct { type TableData struct {
@ -40,6 +44,7 @@ func (t *TableData) Update(rows Rows) {
t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row))
continue continue
} }
if index, ok := t.RowEvents.FindIndex(row.ID); ok { if index, ok := t.RowEvents.FindIndex(row.ID); ok {
delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header.HasAge()) delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header.HasAge())
if delta.IsBlank() { if delta.IsBlank() {
@ -60,6 +65,7 @@ func (t *TableData) Update(rows Rows) {
// Delete delete items in cache that are no longer valid. // Delete delete items in cache that are no longer valid.
func (t *TableData) Delete(newKeys []string) { func (t *TableData) Delete(newKeys []string) {
var victims []string
for _, re := range t.RowEvents { for _, re := range t.RowEvents {
var found bool var found bool
for i, key := range newKeys { for i, key := range newKeys {
@ -70,9 +76,14 @@ func (t *TableData) Delete(newKeys []string) {
} }
} }
if !found { if !found {
t.RowEvents = t.RowEvents.Delete(re.Row.ID) victims = append(victims, re.Row.ID)
} }
} }
for _, id := range victims {
log.Debug().Msgf("Deleting %s", id)
t.RowEvents = t.RowEvents.Delete(id)
}
} }
// Diff checks if two tables are equal. // Diff checks if two tables are equal.

View File

@ -0,0 +1,53 @@
package render_test
import (
"testing"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestTableDataDelete(t *testing.T) {
uu := map[string]struct {
re render.RowEvents
kk []string
e render.RowEvents
}{
"ordered": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
kk: []string{"A", "C"},
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
"unordered": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
{Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}},
},
kk: []string{"C", "A"},
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
}
var table render.TableData
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
table.RowEvents = u.re
table.Delete(u.kk)
assert.Equal(t, u.e, table.RowEvents)
})
}
}

View File

@ -53,7 +53,7 @@ type SelectTable struct {
selectedRow int selectedRow int
selectedFn func(string) string selectedFn func(string) string
selectionListeners []SelectedRowFunc selectionListeners []SelectedRowFunc
marks map[string]bool marks map[string]struct{}
} }
// SetModel sets the table model. // SetModel sets the table model.
@ -86,10 +86,8 @@ func (s *SelectTable) GetSelectedItems() []string {
} }
var items []string var items []string
for item, marked := range s.marks { for item := range s.marks {
if marked { items = append(items, item)
items = append(items, item)
}
} }
return items return items
@ -145,7 +143,7 @@ func (s *SelectTable) selectionChanged(r, c int) {
return return
} }
if s.marks[s.GetSelectedItem()] { if _, ok := s.marks[s.GetSelectedItem()]; ok {
s.SetSelectedStyle(tcell.ColorBlack, tcell.ColorCadetBlue, tcell.AttrBold) s.SetSelectedStyle(tcell.ColorBlack, tcell.ColorCadetBlue, tcell.AttrBold)
} else { } else {
cell := s.GetCell(r, c) cell := s.GetCell(r, c)
@ -171,10 +169,15 @@ func (s *SelectTable) DeleteMark(k string) {
// ToggleMark toggles marked row // ToggleMark toggles marked row
func (s *SelectTable) ToggleMark() { func (s *SelectTable) ToggleMark() {
s.marks[s.GetSelectedItem()] = !s.marks[s.GetSelectedItem()] sel := s.GetSelectedItem()
if !s.marks[s.GetSelectedItem()] { if sel == "" {
return return
} }
if _, ok := s.marks[sel]; ok {
delete(s.marks, s.GetSelectedItem())
} else {
s.marks[sel] = struct{}{}
}
cell := s.GetCell(s.GetSelectedRowIndex(), 0) cell := s.GetCell(s.GetSelectedRowIndex(), 0)
s.SetSelectedStyle( s.SetSelectedStyle(
@ -186,7 +189,8 @@ func (s *SelectTable) ToggleMark() {
// IsMarked returns true if this item was marked. // IsMarked returns true if this item was marked.
func (s *Table) IsMarked(item string) bool { func (s *Table) IsMarked(item string) bool {
return s.marks[item] _, ok := s.marks[item]
return ok
} }
// AddSelectedRowListener add a new selected row listener. // AddSelectedRowListener add a new selected row listener.

View File

@ -45,7 +45,7 @@ func NewTable(gvr string) *Table {
SelectTable: &SelectTable{ SelectTable: &SelectTable{
Table: tview.NewTable(), Table: tview.NewTable(),
model: model.NewTable(gvr), model: model.NewTable(gvr),
marks: make(map[string]bool), marks: make(map[string]struct{}),
}, },
actions: make(KeyActions), actions: make(KeyActions),
cmdBuff: NewCmdBuff('/', FilterBuff), cmdBuff: NewCmdBuff('/', FilterBuff),
@ -277,7 +277,7 @@ func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.Hea
// ClearMarks clear out marked items. // ClearMarks clear out marked items.
func (t *Table) ClearMarks() { func (t *Table) ClearMarks() {
t.marks = map[string]bool{} t.SelectTable.ClearMarks()
t.Refresh() t.Refresh()
} }

View File

@ -357,12 +357,12 @@ func (b *Browser) refreshActions() {
if client.Can(b.meta.Verbs, "delete") { if client.Can(b.meta.Verbs, "delete") {
aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true) aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true)
} }
if client.Can(b.meta.Verbs, "view") {
if !dao.IsK9sMeta(b.meta) {
aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true) aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true)
}
if client.Can(b.meta.Verbs, "describe") {
aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true) aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true)
} }
pluginActions(b, aa) pluginActions(b, aa)
hotKeyActions(b, aa) hotKeyActions(b, aa)
b.Actions().Add(aa) b.Actions().Add(aa)

View File

@ -13,6 +13,6 @@ func TestDeploy(t *testing.T) {
assert.Nil(t, v.Init(makeCtx())) assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "Deployments", v.Name()) assert.Equal(t, "Deployments", v.Name())
assert.Equal(t, 9, len(v.Hints())) assert.Equal(t, 10, len(v.Hints()))
} }

View File

@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
assert.Nil(t, v.Init(makeCtx())) assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "DaemonSets", v.Name()) assert.Equal(t, "DaemonSets", v.Name())
assert.Equal(t, 10, len(v.Hints())) assert.Equal(t, 11, len(v.Hints()))
} }

View File

@ -8,7 +8,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
@ -25,6 +24,8 @@ const (
// Help presents a help viewer. // Help presents a help viewer.
type Help struct { type Help struct {
*Table *Table
maxKey, maxDesc, maxRows int
} }
// NewHelp returns a new help viewer. // NewHelp returns a new help viewer.
@ -44,7 +45,7 @@ func (v *Help) Init(ctx context.Context) error {
v.SetBorder(true) v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1) v.SetBorderPadding(0, 0, 1, 1)
v.bindKeys() v.bindKeys()
v.build(v.app.Content.Top().Hints()) v.build()
v.SetBackgroundColor(v.App().Styles.BgColor()) v.SetBackgroundColor(v.App().Styles.BgColor())
return nil return nil
@ -59,6 +60,40 @@ func (v *Help) bindKeys() {
}) })
} }
func (v *Help) computeMaxes(hh model.MenuHints) {
v.maxKey, v.maxDesc = 0, 0
for _, h := range hh {
if len(h.Mnemonic) > v.maxKey {
v.maxKey = len(h.Mnemonic)
}
if len(h.Description) > v.maxDesc {
v.maxDesc = len(h.Description)
}
}
v.maxKey += 2
}
type HelpFunc func() model.MenuHints
func (v *Help) build() {
v.Clear()
ff := []HelpFunc{v.app.Content.Top().Hints, v.showGeneral, v.showNav, v.showHelp}
var col int
for i, section := range []string{"RESOURCE", "GENERAL", "NAVIGATION", "HELP"} {
hh := ff[i]()
sort.Sort(hh)
v.computeMaxes(hh)
v.addSection(col, section, hh)
col += 2
}
if h, err := v.showHotKeys(); err == nil {
v.computeMaxes(h)
v.addSection(col, "HOTKEYS", h)
}
}
func (v *Help) showHelp() model.MenuHints { func (v *Help) showHelp() model.MenuHints {
return model.MenuHints{ return model.MenuHints{
{ {
@ -186,59 +221,55 @@ func (v *Help) resetTitle() {
v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle)) v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))
} }
func (v *Help) build(hh model.MenuHints) {
v.Clear()
sort.Sort(hh)
var col int
v.addSection(col, "RESOURCE", hh)
col += 2
v.addSection(col, "GENERAL", v.showGeneral())
col += 2
v.addSection(col, "NAVIGATION", v.showNav())
col += 2
if h, err := v.showHotKeys(); err == nil {
v.addSection(col, "HOTKEYS", h)
col += 2
}
v.addSection(col, "HELP", v.showHelp())
}
func (v *Help) addSpacer(c int) { func (v *Help) addSpacer(c int) {
cell := tview.NewTableCell("") cell := tview.NewTableCell(render.Pad("", v.maxKey))
cell.SetBackgroundColor(v.App().Styles.BgColor()) cell.SetBackgroundColor(v.App().Styles.BgColor())
cell.SetExpansion(1) cell.SetExpansion(1)
v.SetCell(0, c, cell) v.SetCell(0, c, cell)
} }
func (v *Help) addSection(c int, title string, hh model.MenuHints) { func (v *Help) addSection(c int, title string, hh model.MenuHints) {
if len(hh) > v.maxRows {
v.maxRows = len(hh)
}
row := 0 row := 0
v.addSpacer(c)
cell := tview.NewTableCell(title) cell := tview.NewTableCell(title)
cell.SetTextColor(tcell.ColorGreen) cell.SetTextColor(tcell.ColorGreen)
cell.SetAttributes(tcell.AttrBold) cell.SetAttributes(tcell.AttrBold)
cell.SetExpansion(1) cell.SetExpansion(1)
cell.SetAlign(tview.AlignLeft) cell.SetAlign(tview.AlignLeft)
v.SetCell(row, c+1, cell) v.SetCell(row, c, cell)
v.addSpacer(c + 1)
row++ row++
for _, h := range hh { for _, h := range hh {
col := c col := c
cell := tview.NewTableCell(toMnemonic(h.Mnemonic)) cell := tview.NewTableCell(render.Pad(toMnemonic(h.Mnemonic), v.maxKey))
if _, err := strconv.Atoi(h.Mnemonic); err != nil { if _, err := strconv.Atoi(h.Mnemonic); err != nil {
cell.SetTextColor(tcell.ColorDodgerBlue) cell.SetTextColor(tcell.ColorDodgerBlue)
} else { } else {
cell.SetTextColor(tcell.ColorFuchsia) cell.SetTextColor(tcell.ColorFuchsia)
} }
cell.SetAttributes(tcell.AttrBold) cell.SetAttributes(tcell.AttrBold)
cell.SetAlign(tview.AlignRight)
v.SetCell(row, col, cell) v.SetCell(row, col, cell)
col++ col++
cell = tview.NewTableCell(h.Description) cell = tview.NewTableCell(render.Pad(h.Description, v.maxDesc))
cell.SetTextColor(tcell.ColorWhite) cell.SetTextColor(tcell.ColorWhite)
v.SetCell(row, col, cell) v.SetCell(row, col, cell)
row++ row++
} }
if len(hh) < v.maxRows {
for i := v.maxRows - len(hh); i > 0; i-- {
col := c
cell := tview.NewTableCell(render.Pad("", v.maxKey))
v.SetCell(row, col, cell)
col++
cell = tview.NewTableCell(render.Pad("", v.maxDesc))
v.SetCell(row, col, cell)
row++
}
}
} }
func toMnemonic(s string) string { func toMnemonic(s string) string {
@ -260,44 +291,3 @@ func keyConv(s string) string {
return strings.Replace(s, "alt", "opt", 1) return strings.Replace(s, "alt", "opt", 1)
} }
func defaultK9sEnv(app *App, sel string, row render.Row) K9sEnv {
ns, n := client.Namespaced(sel)
ctx, err := app.Conn().Config().CurrentContextName()
if err != nil {
ctx = render.NAValue
}
cluster, err := app.Conn().Config().CurrentClusterName()
if err != nil {
cluster = render.NAValue
}
user, err := app.Conn().Config().CurrentUserName()
if err != nil {
user = render.NAValue
}
groups, err := app.Conn().Config().CurrentGroupNames()
if err != nil {
groups = []string{render.NAValue}
}
var cfg string
kcfg := app.Conn().Config().Flags().KubeConfig
if kcfg != nil && *kcfg != "" {
cfg = *kcfg
}
env := K9sEnv{
"NAMESPACE": ns,
"NAME": n,
"CONTEXT": ctx,
"CLUSTER": cluster,
"USER": user,
"GROUPS": strings.Join(groups, ","),
"KUBECONFIG": cfg,
}
for i, r := range row.Fields {
env["COL"+strconv.Itoa(i)] = r
}
return env
}

View File

@ -1,6 +1,7 @@
package view_test package view_test
import ( import (
"strings"
"testing" "testing"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -22,6 +23,6 @@ func TestHelp(t *testing.T) {
assert.Nil(t, v.Init(ctx)) assert.Nil(t, v.Init(ctx))
assert.Equal(t, 17, v.GetRowCount()) assert.Equal(t, 17, v.GetRowCount())
assert.Equal(t, 8, v.GetColumnCount()) assert.Equal(t, 8, v.GetColumnCount())
assert.Equal(t, "<ctrl-k>", v.GetCell(1, 0).Text) assert.Equal(t, "<ctrl-k>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Kill", v.GetCell(1, 1).Text) assert.Equal(t, "Kill", strings.TrimSpace(v.GetCell(1, 1).Text))
} }

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -18,6 +19,47 @@ import (
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
) )
func defaultK9sEnv(app *App, sel string, row render.Row) K9sEnv {
ns, n := client.Namespaced(sel)
ctx, err := app.Conn().Config().CurrentContextName()
if err != nil {
ctx = render.NAValue
}
cluster, err := app.Conn().Config().CurrentClusterName()
if err != nil {
cluster = render.NAValue
}
user, err := app.Conn().Config().CurrentUserName()
if err != nil {
user = render.NAValue
}
groups, err := app.Conn().Config().CurrentGroupNames()
if err != nil {
groups = []string{render.NAValue}
}
var cfg string
kcfg := app.Conn().Config().Flags().KubeConfig
if kcfg != nil && *kcfg != "" {
cfg = *kcfg
}
env := K9sEnv{
"NAMESPACE": ns,
"NAME": n,
"CONTEXT": ctx,
"CLUSTER": cluster,
"USER": user,
"GROUPS": strings.Join(groups, ","),
"KUBECONFIG": cfg,
}
for i, r := range row.Fields {
env["COL"+strconv.Itoa(i)] = r
}
return env
}
func describeResource(app *App, _, gvr, path string) { func describeResource(app *App, _, gvr, path string) {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
yaml, err := dao.Describe(app.Conn(), client.GVR(gvr), ns, n) yaml, err := dao.Describe(app.Conn(), client.GVR(gvr), ns, n)

View File

@ -18,14 +18,14 @@ type RestartExtender struct {
// NewRestartExtender returns a new extender. // NewRestartExtender returns a new extender.
func NewRestartExtender(v ResourceViewer) ResourceViewer { func NewRestartExtender(v ResourceViewer) ResourceViewer {
r := RestartExtender{ResourceViewer: v} r := RestartExtender{ResourceViewer: v}
r.SetBindKeysFn(r.bindKeys) r.bindKeys(v.Actions())
return &r return &r
} }
// BindKeys creates additional menu actions. // BindKeys creates additional menu actions.
func (r *RestartExtender) bindKeys(aa ui.KeyActions) { func (r *RestartExtender) bindKeys(aa ui.KeyActions) {
r.Actions().Add(ui.KeyActions{ aa.Add(ui.KeyActions{
tcell.KeyCtrlT: ui.NewKeyAction("Restart", r.restartCmd, true), tcell.KeyCtrlT: ui.NewKeyAction("Restart", r.restartCmd, true),
}) })
} }

View File

@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx())) assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "StatefulSets", s.Name()) assert.Equal(t, "StatefulSets", s.Name())
assert.Equal(t, 7, len(s.Hints())) assert.Equal(t, 8, len(s.Hints()))
} }