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

View File

@ -218,7 +218,7 @@ func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
crd, ok := o.(*unstructured.Unstructured)
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{}
@ -254,6 +254,7 @@ func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, [
if m[n] == nil {
return nil, errs
}
s, ok := m[n].([]string)
if ok {
return s, errs
@ -268,10 +269,11 @@ func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, [
for i, name := range ii {
ss[i], ok = name.(string)
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) {

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 (
refreshRate = 1 * time.Second
refreshRate = 2 * time.Second
noDataCount = 2
)

View File

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

View File

@ -8,6 +8,58 @@ import (
"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) {
uu := map[string]struct {
re render.RowEvents
@ -32,10 +84,10 @@ func TestSort(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
uc.re.Sort("", uc.col, uc.asc)
assert.Equal(t, uc.e, uc.re)
u.re.Sort("", u.col, u.asc)
assert.Equal(t, u.e, u.re)
})
}
}
@ -52,9 +104,9 @@ func TestDefaultColorer(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
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
import "sync"
import (
"sync"
"github.com/rs/zerolog/log"
)
// TableData tracks a K8s resource for tabular display.
type TableData struct {
@ -40,6 +44,7 @@ func (t *TableData) Update(rows Rows) {
t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row))
continue
}
if index, ok := t.RowEvents.FindIndex(row.ID); ok {
delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header.HasAge())
if delta.IsBlank() {
@ -60,6 +65,7 @@ func (t *TableData) Update(rows Rows) {
// Delete delete items in cache that are no longer valid.
func (t *TableData) Delete(newKeys []string) {
var victims []string
for _, re := range t.RowEvents {
var found bool
for i, key := range newKeys {
@ -70,9 +76,14 @@ func (t *TableData) Delete(newKeys []string) {
}
}
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.

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

View File

@ -45,7 +45,7 @@ func NewTable(gvr string) *Table {
SelectTable: &SelectTable{
Table: tview.NewTable(),
model: model.NewTable(gvr),
marks: make(map[string]bool),
marks: make(map[string]struct{}),
},
actions: make(KeyActions),
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.
func (t *Table) ClearMarks() {
t.marks = map[string]bool{}
t.SelectTable.ClearMarks()
t.Refresh()
}

View File

@ -357,12 +357,12 @@ func (b *Browser) refreshActions() {
if client.Can(b.meta.Verbs, "delete") {
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)
}
if client.Can(b.meta.Verbs, "describe") {
aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true)
}
pluginActions(b, aa)
hotKeyActions(b, aa)
b.Actions().Add(aa)

View File

@ -13,6 +13,6 @@ func TestDeploy(t *testing.T) {
assert.Nil(t, v.Init(makeCtx()))
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.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"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
@ -25,6 +24,8 @@ const (
// Help presents a help viewer.
type Help struct {
*Table
maxKey, maxDesc, maxRows int
}
// NewHelp returns a new help viewer.
@ -44,7 +45,7 @@ func (v *Help) Init(ctx context.Context) error {
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
v.bindKeys()
v.build(v.app.Content.Top().Hints())
v.build()
v.SetBackgroundColor(v.App().Styles.BgColor())
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 {
return model.MenuHints{
{
@ -186,59 +221,55 @@ func (v *Help) resetTitle() {
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) {
cell := tview.NewTableCell("")
cell := tview.NewTableCell(render.Pad("", v.maxKey))
cell.SetBackgroundColor(v.App().Styles.BgColor())
cell.SetExpansion(1)
v.SetCell(0, c, cell)
}
func (v *Help) addSection(c int, title string, hh model.MenuHints) {
if len(hh) > v.maxRows {
v.maxRows = len(hh)
}
row := 0
v.addSpacer(c)
cell := tview.NewTableCell(title)
cell.SetTextColor(tcell.ColorGreen)
cell.SetAttributes(tcell.AttrBold)
cell.SetExpansion(1)
cell.SetAlign(tview.AlignLeft)
v.SetCell(row, c+1, cell)
v.SetCell(row, c, cell)
v.addSpacer(c + 1)
row++
for _, h := range hh {
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 {
cell.SetTextColor(tcell.ColorDodgerBlue)
} else {
cell.SetTextColor(tcell.ColorFuchsia)
}
cell.SetAttributes(tcell.AttrBold)
cell.SetAlign(tview.AlignRight)
v.SetCell(row, col, cell)
col++
cell = tview.NewTableCell(h.Description)
cell = tview.NewTableCell(render.Pad(h.Description, v.maxDesc))
cell.SetTextColor(tcell.ColorWhite)
v.SetCell(row, col, cell)
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 {
@ -260,44 +291,3 @@ func keyConv(s string) string {
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
import (
"strings"
"testing"
"github.com/derailed/k9s/internal"
@ -22,6 +23,6 @@ func TestHelp(t *testing.T) {
assert.Nil(t, v.Init(ctx))
assert.Equal(t, 17, v.GetRowCount())
assert.Equal(t, 8, v.GetColumnCount())
assert.Equal(t, "<ctrl-k>", v.GetCell(1, 0).Text)
assert.Equal(t, "Kill", v.GetCell(1, 1).Text)
assert.Equal(t, "<ctrl-k>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Kill", strings.TrimSpace(v.GetCell(1, 1).Text))
}

View File

@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"github.com/derailed/k9s/internal"
@ -18,6 +19,47 @@ import (
"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) {
ns, n := client.Namespaced(path)
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.
func NewRestartExtender(v ResourceViewer) ResourceViewer {
r := RestartExtender{ResourceViewer: v}
r.SetBindKeysFn(r.bindKeys)
r.bindKeys(v.Actions())
return &r
}
// BindKeys creates additional menu actions.
func (r *RestartExtender) bindKeys(aa ui.KeyActions) {
r.Actions().Add(ui.KeyActions{
aa.Add(ui.KeyActions{
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.Equal(t, "StatefulSets", s.Name())
assert.Equal(t, 7, len(s.Hints()))
assert.Equal(t, 8, len(s.Hints()))
}