parent
ee01ae7242
commit
4c222f80ed
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
refreshRate = 1 * time.Second
|
||||
refreshRate = 2 * time.Second
|
||||
noDataCount = 2
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue