checkpoint
parent
42e2e98e4d
commit
543f9837ff
Binary file not shown.
|
After Width: | Height: | Size: 11 MiB |
|
|
@ -0,0 +1,64 @@
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||||
|
|
||||||
|
# Release v0.17.0
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/beach.png" align="center"/>
|
||||||
|
|
||||||
|
## Custom Columns? Yes Please!!
|
||||||
|
|
||||||
|
[SneakCast v0.17.0 on The Beach! - Yup! sound is sucking but what a setting!](https://youtu.be/7S33CNLAofk)
|
||||||
|
|
||||||
|
In this drop I've reworked the rendering engine to provide for custom columns support. Now, you should be able to not only tell K9s which columns you would like to display but also which order they should be in.
|
||||||
|
|
||||||
|
To surface this feature, you will need to create a new configuration file, namely `$HOME/.k9s/views.yml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live!
|
||||||
|
|
||||||
|
> NOTE: This is experimental and will most likely change as we iron this out!
|
||||||
|
|
||||||
|
Here is a sample views configuration that customize a pods and services views.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# $HOME/.k9s/views.yml
|
||||||
|
k9s:
|
||||||
|
views:
|
||||||
|
v1/pods:
|
||||||
|
columns:
|
||||||
|
- AGE
|
||||||
|
- NAMESPACE
|
||||||
|
- NAME
|
||||||
|
- IP
|
||||||
|
- NODE
|
||||||
|
- STATUS
|
||||||
|
- READY
|
||||||
|
v1/services:
|
||||||
|
columns:
|
||||||
|
- AGE
|
||||||
|
- NAMESPACE
|
||||||
|
- NAME
|
||||||
|
- TYPE
|
||||||
|
- CLUSTER-IP
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolved Bugs/Features/PRs
|
||||||
|
|
||||||
|
- [Issue #581](https://github.com/derailed/k9s/issues/581)
|
||||||
|
- [Issue #576](https://github.com/derailed/k9s/issues/576)
|
||||||
|
- [Issue #574](https://github.com/derailed/k9s/issues/574)
|
||||||
|
- [Issue #573](https://github.com/derailed/k9s/issues/573)
|
||||||
|
- [Issue #571](https://github.com/derailed/k9s/issues/571)
|
||||||
|
- [Issue #566](https://github.com/derailed/k9s/issues/566)
|
||||||
|
- [Issue #563](https://github.com/derailed/k9s/issues/563)
|
||||||
|
- [Issue #562](https://github.com/derailed/k9s/issues/562)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
@ -13,7 +13,7 @@ const (
|
||||||
// DefaultDirMod default unix perms for k9s directory.
|
// DefaultDirMod default unix perms for k9s directory.
|
||||||
DefaultDirMod os.FileMode = 0755
|
DefaultDirMod os.FileMode = 0755
|
||||||
// DefaultFileMod default unix perms for k9s files.
|
// DefaultFileMod default unix perms for k9s files.
|
||||||
DefaultFileMod os.FileMode = 0644
|
DefaultFileMod os.FileMode = 0600
|
||||||
)
|
)
|
||||||
|
|
||||||
// InList check if string is in a collection of strings.
|
// InList check if string is in a collection of strings.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var K9sViewConfigFile = filepath.Join(K9sHome, "views_config.yml")
|
var K9sViewConfigFile = filepath.Join(K9sHome, "views.yml")
|
||||||
|
|
||||||
// ViewConfigListener represents a view config listener.
|
// ViewConfigListener represents a view config listener.
|
||||||
type ViewConfigListener interface {
|
type ViewConfigListener interface {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func (d *Deployment) Scale(path string, replicas int32) error {
|
||||||
|
|
||||||
// Restart a Deployment rollout.
|
// Restart a Deployment rollout.
|
||||||
func (d *Deployment) Restart(path string) error {
|
func (d *Deployment) Restart(path string) error {
|
||||||
dp, err := d.GetInstance(path)
|
dp, err := d.Load(d.Factory, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +81,7 @@ func (d *Deployment) Restart(path string) error {
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this Deployment.
|
// TailLogs tail logs for all pods represented by this Deployment.
|
||||||
func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||||
dp, err := d.GetInstance(opts.Path)
|
dp, err := d.Load(d.Factory, opts.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +94,7 @@ func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOpti
|
||||||
|
|
||||||
// Pod returns a pod victim by name.
|
// Pod returns a pod victim by name.
|
||||||
func (d *Deployment) Pod(fqn string) (string, error) {
|
func (d *Deployment) Pod(fqn string) (string, error) {
|
||||||
dp, err := d.GetInstance(fqn)
|
dp, err := d.Load(d.Factory, fqn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -103,8 +103,8 @@ func (d *Deployment) Pod(fqn string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInstance returns a deployment instance.
|
// GetInstance returns a deployment instance.
|
||||||
func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) {
|
func (*Deployment) Load(f Factory, fqn string) (*appsv1.Deployment, error) {
|
||||||
o, err := d.Factory.Get(d.gvr.String(), fqn, false, labels.Everything())
|
o, err := f.Get("apps/v1/deployments", fqn, false, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@ func deleteFunctionToken(gateway string, functionName string, tlsInsecure bool,
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", deleteEndpoint, reader)
|
req, err := http.NewRequest("DELETE", deleteEndpoint, reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
@ -172,9 +171,7 @@ func deleteFunctionToken(gateway string, functionName string, tlsInsecure bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
delRes, delErr := c.Do(req)
|
delRes, delErr := c.Do(req)
|
||||||
|
|
||||||
if delErr != nil {
|
if delErr != nil {
|
||||||
fmt.Printf("Error removing existing function: %s, gateway=%s, functionName=%s\n", delErr.Error(), gateway, functionName)
|
|
||||||
return delErr
|
return delErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns a resource instance if found, else an error.
|
// Get returns a resource instance if found, else an error.
|
||||||
func (r *Resource) Get(ctx context.Context, path string) (runtime.Object, error) {
|
func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) {
|
||||||
return r.Factory.Get(r.gvr.String(), path, true, labels.Everything())
|
return r.Factory.Get(r.gvr.String(), path, true, labels.Everything())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,19 @@
|
||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
v1 "k8s.io/api/apps/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReplicaSet represents a replicaset K8s resource.
|
// ReplicaSet represents a replicaset K8s resource.
|
||||||
|
|
@ -9,15 +21,96 @@ type ReplicaSet struct {
|
||||||
Resource
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHappy check for happy deployments.
|
// BOZO!!
|
||||||
func (d *ReplicaSet) IsHappy(rs appsv1.ReplicaSet) bool {
|
// // IsHappy check for happy deployments.
|
||||||
if rs.Status.Replicas == 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
// func (*ReplicaSet) IsHappy(rs appsv1.ReplicaSet) bool {
|
||||||
return false
|
// if rs.Status.Replicas == 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if rs.Status.Replicas != 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (r *ReplicaSet) Load(f Factory, path string) (*v1.ReplicaSet, error) {
|
||||||
|
o, err := f.Get("apps/v1/replicasets", path, true, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if rs.Status.Replicas != 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
var rs appsv1.ReplicaSet
|
||||||
return false
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return &rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRSRevision(rs *v1.ReplicaSet) (int64, error) {
|
||||||
|
revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"]
|
||||||
|
if rs.Status.Replicas != 0 {
|
||||||
|
return 0, errors.New("can not rollback current replica")
|
||||||
|
}
|
||||||
|
vers, err := strconv.Atoi(revision)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("revision conversion failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(vers), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func controllerInfo(rs *v1.ReplicaSet) (string, string, string, error) {
|
||||||
|
for _, ref := range rs.ObjectMeta.OwnerReferences {
|
||||||
|
if ref.Controller == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group, tokens := ref.APIVersion, strings.Split(ref.APIVersion, "/")
|
||||||
|
if len(tokens) == 2 {
|
||||||
|
group = tokens[0]
|
||||||
|
}
|
||||||
|
return ref.Name, ref.Kind, group, nil
|
||||||
|
}
|
||||||
|
return "", "", "", fmt.Errorf("Unable to find controller for ReplicaSet %s", rs.ObjectMeta.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback reverses the last deployment.
|
||||||
|
func (r *ReplicaSet) Rollback(fqn string) error {
|
||||||
|
rs, err := r.Load(r.Factory, fqn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := getRSRevision(rs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name, kind, apiGroup, err := controllerInfo(rs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{
|
||||||
|
Group: apiGroup,
|
||||||
|
Kind: kind},
|
||||||
|
r.Client().DialOrDie(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ddp Deployment
|
||||||
|
dp, err := ddp.Load(r.Factory, client.FQN(rs.Namespace, name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rb.Rollback(dp, map[string]string{}, version, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ func TestTableGenericHydrate(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, genericHydrate("blee", &tt, rr, &re))
|
assert.Nil(t, genericHydrate("blee", &tt, rr, &re))
|
||||||
assert.Equal(t, 2, len(rr))
|
assert.Equal(t, 2, len(rr))
|
||||||
assert.Equal(t, 2, len(rr[0].Fields))
|
assert.Equal(t, 3, len(rr[0].Fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ func ToContainerState(s v1.ContainerState) string {
|
||||||
if s.Terminated.Reason != "" {
|
if s.Terminated.Reason != "" {
|
||||||
return s.Terminated.Reason
|
return s.Terminated.Reason
|
||||||
}
|
}
|
||||||
return "Terminated"
|
return "Terminating"
|
||||||
case s.Running != nil:
|
case s.Running != nil:
|
||||||
return "Running"
|
return "Running"
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,10 @@ func (d DeltaRow) Customize(cols []int, out DeltaRow) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i, c := range cols {
|
for i, c := range cols {
|
||||||
if c < len(d) {
|
if c < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c < len(d) && i < len(out) {
|
||||||
out[i] = d[c]
|
out[i] = d[c]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,16 @@ func TestDeltaCustomize(t *testing.T) {
|
||||||
cols: []int{2, 10, 0},
|
cols: []int{2, 10, 0},
|
||||||
e: render.DeltaRow{"c", "", "a"},
|
e: render.DeltaRow{"c", "", "a"},
|
||||||
},
|
},
|
||||||
|
"diff-negative": {
|
||||||
|
r1: render.Row{
|
||||||
|
Fields: render.Fields{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
r2: render.Row{
|
||||||
|
Fields: render.Fields{"a1", "b1", "c1"},
|
||||||
|
},
|
||||||
|
cols: []int{2, -1, 0},
|
||||||
|
e: render.DeltaRow{"c", "", "a"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,9 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Deployment) diagnose(d, r int32) error {
|
func (Deployment) diagnose(desired, avail int32) error {
|
||||||
if d != r {
|
if desired != avail {
|
||||||
return fmt.Errorf("desiring %d replicas got %d available", d, r)
|
return fmt.Errorf("desiring %d replicas got %d available", desired, avail)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ func (g *Generic) Header(ns string) Header {
|
||||||
return Header{}
|
return Header{}
|
||||||
}
|
}
|
||||||
h := make(Header, 0, len(g.table.ColumnDefinitions))
|
h := make(Header, 0, len(g.table.ColumnDefinitions))
|
||||||
|
h = append(h, HeaderColumn{Name: "NAMESPACE"})
|
||||||
for i, c := range g.table.ColumnDefinitions {
|
for i, c := range g.table.ColumnDefinitions {
|
||||||
if c.Name == ageTableCol {
|
if c.Name == ageTableCol {
|
||||||
g.ageIndex = i
|
g.ageIndex = i
|
||||||
|
|
@ -48,7 +49,7 @@ func (g *Generic) Header(ns string) Header {
|
||||||
h = append(h, HeaderColumn{Name: strings.ToUpper(c.Name)})
|
h = append(h, HeaderColumn{Name: strings.ToUpper(c.Name)})
|
||||||
}
|
}
|
||||||
if g.ageIndex > 0 {
|
if g.ageIndex > 0 {
|
||||||
h = append(h, HeaderColumn{Name: "AGE", Time: true,})
|
h = append(h, HeaderColumn{Name: "AGE", Time: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
|
@ -72,9 +73,7 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
|
||||||
r.ID = client.FQN(nns, n)
|
r.ID = client.FQN(nns, n)
|
||||||
r.Fields = make(Fields, 0, len(g.Header(ns)))
|
r.Fields = make(Fields, 0, len(g.Header(ns)))
|
||||||
if client.IsAllNamespaces(ns) && nns != "" {
|
r.Fields = append(r.Fields, nns)
|
||||||
r.Fields = append(r.Fields, nns)
|
|
||||||
}
|
|
||||||
var ageCell interface{}
|
var ageCell interface{}
|
||||||
for i, c := range row.Cells {
|
for i, c := range row.Cells {
|
||||||
if g.ageIndex > 0 && i == g.ageIndex {
|
if g.ageIndex > 0 && i == g.ageIndex {
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,9 @@ func TestGenericRender(t *testing.T) {
|
||||||
ns: "ns1",
|
ns: "ns1",
|
||||||
table: makeNSGeneric(),
|
table: makeNSGeneric(),
|
||||||
eID: "ns1/c1",
|
eID: "ns1/c1",
|
||||||
eFields: render.Fields{"c1", "c2", "c3"},
|
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
|
||||||
eHeader: render.Header{
|
eHeader: render.Header{
|
||||||
|
render.HeaderColumn{Name: "NAMESPACE"},
|
||||||
render.HeaderColumn{Name: "A"},
|
render.HeaderColumn{Name: "A"},
|
||||||
render.HeaderColumn{Name: "B"},
|
render.HeaderColumn{Name: "B"},
|
||||||
render.HeaderColumn{Name: "C"},
|
render.HeaderColumn{Name: "C"},
|
||||||
|
|
@ -35,7 +36,7 @@ func TestGenericRender(t *testing.T) {
|
||||||
eID: "ns1/c1",
|
eID: "ns1/c1",
|
||||||
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
|
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
|
||||||
eHeader: render.Header{
|
eHeader: render.Header{
|
||||||
// render.HeaderColumn{Name: "NAMESPACE"},
|
render.HeaderColumn{Name: "NAMESPACE"},
|
||||||
render.HeaderColumn{Name: "A"},
|
render.HeaderColumn{Name: "A"},
|
||||||
render.HeaderColumn{Name: "B"},
|
render.HeaderColumn{Name: "B"},
|
||||||
render.HeaderColumn{Name: "C"},
|
render.HeaderColumn{Name: "C"},
|
||||||
|
|
@ -47,7 +48,7 @@ func TestGenericRender(t *testing.T) {
|
||||||
eID: "ns1/c1",
|
eID: "ns1/c1",
|
||||||
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
|
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
|
||||||
eHeader: render.Header{
|
eHeader: render.Header{
|
||||||
// render.HeaderColumn{Name: "NAMESPACE"},
|
render.HeaderColumn{Name: "NAMESPACE"},
|
||||||
render.HeaderColumn{Name: "A"},
|
render.HeaderColumn{Name: "A"},
|
||||||
render.HeaderColumn{Name: "B"},
|
render.HeaderColumn{Name: "B"},
|
||||||
render.HeaderColumn{Name: "C"},
|
render.HeaderColumn{Name: "C"},
|
||||||
|
|
@ -57,8 +58,9 @@ func TestGenericRender(t *testing.T) {
|
||||||
ns: client.ClusterScope,
|
ns: client.ClusterScope,
|
||||||
table: makeNoNSGeneric(),
|
table: makeNoNSGeneric(),
|
||||||
eID: "-/c1",
|
eID: "-/c1",
|
||||||
eFields: render.Fields{"c1", "c2", "c3"},
|
eFields: render.Fields{"-", "c1", "c2", "c3"},
|
||||||
eHeader: render.Header{
|
eHeader: render.Header{
|
||||||
|
render.HeaderColumn{Name: "NAMESPACE"},
|
||||||
render.HeaderColumn{Name: "A"},
|
render.HeaderColumn{Name: "A"},
|
||||||
render.HeaderColumn{Name: "B"},
|
render.HeaderColumn{Name: "B"},
|
||||||
render.HeaderColumn{Name: "C"},
|
render.HeaderColumn{Name: "C"},
|
||||||
|
|
@ -68,11 +70,12 @@ func TestGenericRender(t *testing.T) {
|
||||||
ns: client.ClusterScope,
|
ns: client.ClusterScope,
|
||||||
table: makeAgeGeneric(),
|
table: makeAgeGeneric(),
|
||||||
eID: "-/c1",
|
eID: "-/c1",
|
||||||
eFields: render.Fields{"c1", "c2", "Age"},
|
eFields: render.Fields{"-", "c1", "c2", "Age"},
|
||||||
eHeader: render.Header{
|
eHeader: render.Header{
|
||||||
|
render.HeaderColumn{Name: "NAMESPACE"},
|
||||||
render.HeaderColumn{Name: "A"},
|
render.HeaderColumn{Name: "A"},
|
||||||
render.HeaderColumn{Name: "C"},
|
render.HeaderColumn{Name: "C"},
|
||||||
render.HeaderColumn{Name: "AGE", Time: true,},
|
render.HeaderColumn{Name: "AGE", Time: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,28 +40,27 @@ func (h Header) Clone() Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapIndices returns a collection of mapped column indices based of the requested columns.
|
// MapIndices returns a collection of mapped column indices based of the requested columns.
|
||||||
func (h Header) MapIndices(cols []string, wide bool, ii []int) {
|
func (h Header) MapIndices(cols []string, wide bool) []int {
|
||||||
|
ii := make([]int, 0, len(cols))
|
||||||
cc := make(map[int]struct{}, len(cols))
|
cc := make(map[int]struct{}, len(cols))
|
||||||
var lastIndex int
|
for _, col := range cols {
|
||||||
log.Debug().Msgf("MAP %d -- %d ", len(cols), len(ii))
|
|
||||||
for i, col := range cols {
|
|
||||||
idx := h.IndexOf(col, true)
|
idx := h.IndexOf(col, true)
|
||||||
ii[i], cc[idx] = idx, struct{}{}
|
if idx < 0 {
|
||||||
lastIndex = i
|
log.Warn().Msgf("Column %q not found on resource", col)
|
||||||
|
}
|
||||||
|
ii, cc[idx] = append(ii, idx), struct{}{}
|
||||||
}
|
}
|
||||||
if !wide {
|
if !wide {
|
||||||
return
|
return ii
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range h {
|
for i := range h {
|
||||||
if _, ok := cc[i]; ok {
|
if _, ok := cc[i]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lastIndex++
|
ii = append(ii, i)
|
||||||
if lastIndex < len(ii) {
|
|
||||||
ii[lastIndex] = i
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return ii
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customize builds a header from custom col definitions.
|
// Customize builds a header from custom col definitions.
|
||||||
|
|
@ -73,8 +72,13 @@ func (h Header) Customize(cols []string, wide bool) Header {
|
||||||
xx := make(map[int]struct{}, len(h))
|
xx := make(map[int]struct{}, len(h))
|
||||||
for _, c := range cols {
|
for _, c := range cols {
|
||||||
idx := h.IndexOf(c, true)
|
idx := h.IndexOf(c, true)
|
||||||
|
// BOZO!!
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
log.Warn().Msgf("Column %s is not available on this resource", c)
|
log.Warn().Msgf("Column %s is not available on this resource", c)
|
||||||
|
col := HeaderColumn{
|
||||||
|
Name: c,
|
||||||
|
}
|
||||||
|
cc = append(cc, col)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
xx[idx] = struct{}{}
|
xx[idx] = struct{}{}
|
||||||
|
|
@ -153,3 +157,11 @@ func (h Header) IndexOf(colName string, includeWide bool) int {
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dump for debuging.
|
||||||
|
func (h Header) Dump() {
|
||||||
|
log.Debug().Msgf("HEADER")
|
||||||
|
for i, c := range h {
|
||||||
|
log.Debug().Msgf("%d %q -- %t", i, c.Name, c.Wide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,7 @@ func TestHeaderMapIndices(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
ii := make([]int, len(u.cols))
|
ii := u.h1.MapIndices(u.cols, u.wide)
|
||||||
u.h1.MapIndices(u.cols, u.wide, ii)
|
|
||||||
assert.Equal(t, u.e, ii)
|
assert.Equal(t, u.e, ii)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +143,7 @@ func TestHeaderCustomize(t *testing.T) {
|
||||||
cols: []string{"BLEE", "A"},
|
cols: []string{"BLEE", "A"},
|
||||||
wide: true,
|
wide: true,
|
||||||
e: render.Header{
|
e: render.Header{
|
||||||
|
render.HeaderColumn{Name: "BLEE"},
|
||||||
render.HeaderColumn{Name: "A"},
|
render.HeaderColumn{Name: "A"},
|
||||||
render.HeaderColumn{Name: "B", Wide: true},
|
render.HeaderColumn{Name: "B", Wide: true},
|
||||||
render.HeaderColumn{Name: "C", Wide: true},
|
render.HeaderColumn{Name: "C", Wide: true},
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ func (p Pod) diagnose(phase string, cr, ct int) error {
|
||||||
if phase == "Completed" {
|
if phase == "Completed" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cr != ct {
|
if cr != ct || ct == 0 {
|
||||||
return fmt.Errorf("container ready check failed: %d of %d", cr, ct)
|
return fmt.Errorf("container ready check failed: %d of %d", cr, ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ func (p *Pod) Phase(po *v1.Pod) string {
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Terminated"
|
return "Terminating"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) {
|
func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) {
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
|
||||||
phase := pv.Status.Phase
|
phase := pv.Status.Phase
|
||||||
if pv.ObjectMeta.DeletionTimestamp != nil {
|
if pv.ObjectMeta.DeletionTimestamp != nil {
|
||||||
phase = "Terminating"
|
phase = "Terminated"
|
||||||
}
|
}
|
||||||
var claim string
|
var claim string
|
||||||
if pv.Spec.ClaimRef != nil {
|
if pv.Spec.ClaimRef != nil {
|
||||||
|
|
@ -92,22 +92,22 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
|
||||||
size.String(),
|
size.String(),
|
||||||
accessMode(pv.Spec.AccessModes),
|
accessMode(pv.Spec.AccessModes),
|
||||||
string(pv.Spec.PersistentVolumeReclaimPolicy),
|
string(pv.Spec.PersistentVolumeReclaimPolicy),
|
||||||
string(phase),
|
string(pv.Status.Phase),
|
||||||
claim,
|
claim,
|
||||||
class,
|
class,
|
||||||
pv.Status.Reason,
|
pv.Status.Reason,
|
||||||
p.volumeMode(pv.Spec.VolumeMode),
|
p.volumeMode(pv.Spec.VolumeMode),
|
||||||
mapToStr(pv.Labels),
|
mapToStr(pv.Labels),
|
||||||
asStatus(p.diagnose(string(phase))),
|
asStatus(p.diagnose(phase)),
|
||||||
toAge(pv.ObjectMeta.CreationTimestamp),
|
toAge(pv.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (PersistentVolume) diagnose(r string) error {
|
func (PersistentVolume) diagnose(phase v1.PersistentVolumePhase) error {
|
||||||
if r != "Bound" && r != "Available" {
|
if phase == v1.VolumeFailed {
|
||||||
return fmt.Errorf("unexpected status %s", r)
|
return fmt.Errorf("failed to delete or recycle")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ type Fields []string
|
||||||
func (f Fields) Customize(cols []int, out Fields) {
|
func (f Fields) Customize(cols []int, out Fields) {
|
||||||
for i, c := range cols {
|
for i, c := range cols {
|
||||||
if c < 0 {
|
if c < 0 {
|
||||||
out[i] = "<TOAST!>"
|
out[i] = NAValue
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if c < len(f) {
|
if c < len(f) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import "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 {
|
||||||
Header Header
|
Header Header
|
||||||
|
|
@ -19,9 +17,7 @@ func (t *TableData) Customize(cols []string, wide bool) TableData {
|
||||||
Namespace: t.Namespace,
|
Namespace: t.Namespace,
|
||||||
Header: t.Header.Customize(cols, wide),
|
Header: t.Header.Customize(cols, wide),
|
||||||
}
|
}
|
||||||
ids := make([]int, len(t.Header))
|
ids := t.Header.MapIndices(cols, wide)
|
||||||
t.Header.MapIndices(cols, wide, ids)
|
|
||||||
log.Debug().Msgf("INDICES %#v", ids)
|
|
||||||
res.RowEvents = t.RowEvents.Customize(ids)
|
res.RowEvents = t.RowEvents.Customize(ids)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ func NewApp(context string) *App {
|
||||||
// Init initializes the application.
|
// Init initializes the application.
|
||||||
func (a *App) Init() {
|
func (a *App) Init() {
|
||||||
a.bindKeys()
|
a.bindKeys()
|
||||||
a.SetInputCapture(a.keyboard)
|
|
||||||
a.cmdBuff.AddListener(a.Cmd())
|
a.cmdBuff.AddListener(a.Cmd())
|
||||||
a.Styles.AddListener(a)
|
a.Styles.AddListener(a)
|
||||||
a.CmdBuff().AddListener(a)
|
a.CmdBuff().AddListener(a)
|
||||||
|
|
@ -153,6 +152,11 @@ func (a *App) InCmdMode() bool {
|
||||||
return a.Cmd().InCmdMode()
|
return a.Cmd().InCmdMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) HasAction(key tcell.Key) (KeyAction, bool) {
|
||||||
|
act, ok := a.actions[key]
|
||||||
|
return act, ok
|
||||||
|
}
|
||||||
|
|
||||||
// GetActions returns a collection of actiona.
|
// GetActions returns a collection of actiona.
|
||||||
func (a *App) GetActions() KeyActions {
|
func (a *App) GetActions() KeyActions {
|
||||||
return a.actions
|
return a.actions
|
||||||
|
|
@ -170,23 +174,6 @@ func (a *App) Views() map[string]tview.Primitive {
|
||||||
return a.views
|
return a.views
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|
||||||
key := evt.Key()
|
|
||||||
if key == tcell.KeyRune {
|
|
||||||
if a.cmdBuff.IsActive() && evt.Modifiers() == tcell.ModNone {
|
|
||||||
a.cmdBuff.Add(evt.Rune())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
key = AsKey(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a, ok := a.actions[key]; ok {
|
|
||||||
return a.Action(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
return evt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if !a.CmdBuff().IsActive() {
|
if !a.CmdBuff().IsActive() {
|
||||||
return evt
|
return evt
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,17 @@ func NewPages() *Pages {
|
||||||
return &p
|
return &p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsTopDialog checks if front page is a dialog.
|
||||||
|
func (p *Pages) IsTopDialog() bool {
|
||||||
|
_, pa := p.GetFrontPage()
|
||||||
|
switch pa.(type) {
|
||||||
|
case *tview.ModalForm:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show displays a given page.
|
// Show displays a given page.
|
||||||
func (p *Pages) Show(c model.Component) {
|
func (p *Pages) Show(c model.Component) {
|
||||||
p.SwitchToPage(componentID(c))
|
p.SwitchToPage(componentID(c))
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,6 @@ func (t *Table) Init(ctx context.Context) {
|
||||||
t.SetBorderPadding(0, 0, 1, 1)
|
t.SetBorderPadding(0, 0, 1, 1)
|
||||||
t.SetSelectable(true, false)
|
t.SetSelectable(true, false)
|
||||||
t.SetSelectionChangedFunc(t.selectionChanged)
|
t.SetSelectionChangedFunc(t.selectionChanged)
|
||||||
t.SetInputCapture(t.keyboard)
|
|
||||||
t.SetBackgroundColor(tcell.ColorDefault)
|
t.SetBackgroundColor(tcell.ColorDefault)
|
||||||
|
|
||||||
if cfg, ok := ctx.Value(internal.KeyViewConfig).(*config.CustomView); ok && cfg != nil {
|
if cfg, ok := ctx.Value(internal.KeyViewConfig).(*config.CustomView); ok && cfg != nil {
|
||||||
|
|
@ -129,12 +128,7 @@ func (t *Table) Styles() *config.Styles {
|
||||||
return t.styles
|
return t.styles
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendKey sends an keyboard event (testing only!).
|
func (t *Table) FilterInput(r rune) bool {
|
||||||
func (t *Table) SendKey(evt *tcell.EventKey) {
|
|
||||||
t.keyboard(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Table) filterInput(r rune) bool {
|
|
||||||
if !t.cmdBuff.IsActive() {
|
if !t.cmdBuff.IsActive() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -147,26 +141,6 @@ func (t *Table) filterInput(r rune) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|
||||||
key := evt.Key()
|
|
||||||
if key == tcell.KeyUp || key == tcell.KeyDown {
|
|
||||||
return evt
|
|
||||||
}
|
|
||||||
|
|
||||||
if key == tcell.KeyRune {
|
|
||||||
if t.filterInput(evt.Rune()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
key = AsKey(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a, ok := t.actions[key]; ok {
|
|
||||||
return a.Action(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
return evt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hints returns the view hints.
|
// Hints returns the view hints.
|
||||||
func (t *Table) Hints() model.MenuHints {
|
func (t *Table) Hints() model.MenuHints {
|
||||||
return t.actions.Hints()
|
return t.actions.Hints()
|
||||||
|
|
@ -214,7 +188,6 @@ func (t *Table) doUpdate(data render.TableData) {
|
||||||
t.actions.Delete(KeyShiftP)
|
t.actions.Delete(KeyShiftP)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMX := t.model.HasMetrics()
|
|
||||||
var cols []string
|
var cols []string
|
||||||
if t.viewSetting != nil {
|
if t.viewSetting != nil {
|
||||||
cols = t.viewSetting.Columns
|
cols = t.viewSetting.Columns
|
||||||
|
|
@ -222,19 +195,19 @@ func (t *Table) doUpdate(data render.TableData) {
|
||||||
if len(cols) == 0 {
|
if len(cols) == 0 {
|
||||||
cols = t.header.Columns(t.wide)
|
cols = t.header.Columns(t.wide)
|
||||||
}
|
}
|
||||||
data = data.Customize(cols, t.wide)
|
custData := data.Customize(cols, t.wide)
|
||||||
|
|
||||||
if t.sortCol.name == "" || data.Header.IndexOf(t.sortCol.name, false) == -1 {
|
if t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1 {
|
||||||
t.sortCol.name = data.Header[0].Name
|
t.sortCol.name = custData.Header[0].Name
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Clear()
|
t.Clear()
|
||||||
fg := t.styles.Table().Header.FgColor.Color()
|
fg := t.styles.Table().Header.FgColor.Color()
|
||||||
bg := t.styles.Table().Header.BgColor.Color()
|
bg := t.styles.Table().Header.BgColor.Color()
|
||||||
|
|
||||||
|
hasMX := t.model.HasMetrics()
|
||||||
var col int
|
var col int
|
||||||
fmt.Printf("NS %q\n", t.GetModel().GetNamespace())
|
for _, h := range custData.Header {
|
||||||
for _, h := range data.Header {
|
|
||||||
if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
|
if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -247,29 +220,32 @@ func (t *Table) doUpdate(data render.TableData) {
|
||||||
c.SetTextColor(fg)
|
c.SetTextColor(fg)
|
||||||
col++
|
col++
|
||||||
}
|
}
|
||||||
data.RowEvents.Sort(data.Namespace, data.Header.IndexOf(t.sortCol.name, false), t.sortCol.name == "AGE", t.sortCol.asc)
|
custData.RowEvents.Sort(custData.Namespace, custData.Header.IndexOf(t.sortCol.name, false), t.sortCol.name == "AGE", t.sortCol.asc)
|
||||||
|
|
||||||
pads := make(MaxyPad, len(data.Header))
|
pads := make(MaxyPad, len(custData.Header))
|
||||||
ComputeMaxColumns(pads, t.sortCol.name, data.Header, data.RowEvents)
|
ComputeMaxColumns(pads, t.sortCol.name, custData.Header, custData.RowEvents)
|
||||||
for row, re := range data.RowEvents {
|
for row, re := range custData.RowEvents {
|
||||||
t.buildRow(row+1, re, data.Header, pads, hasMX)
|
idx, _ := data.RowEvents.FindIndex(re.Row.ID)
|
||||||
|
t.buildRow(row+1, re, data.RowEvents[idx], custData.Header, pads)
|
||||||
}
|
}
|
||||||
t.updateSelection(true)
|
t.updateSelection(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) buildRow(r int, re render.RowEvent, h render.Header, pads MaxyPad, hasMX bool) {
|
func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads MaxyPad) {
|
||||||
color := render.DefaultColorer
|
color := render.DefaultColorer
|
||||||
if t.colorerFn != nil {
|
if t.colorerFn != nil {
|
||||||
color = t.colorerFn
|
color = t.colorerFn
|
||||||
}
|
}
|
||||||
|
|
||||||
marked := t.IsMarked(re.Row.ID)
|
marked := t.IsMarked(re.Row.ID)
|
||||||
|
hasMX := t.model.HasMetrics()
|
||||||
var col int
|
var col int
|
||||||
for c, field := range re.Row.Fields {
|
for c, field := range re.Row.Fields {
|
||||||
if c >= len(h) {
|
if c >= len(h) {
|
||||||
log.Error().Msgf("field/header overflow detected for %d::%d. Check your mappings!", c, len(h))
|
log.Error().Msgf("field/header overflow detected for %q -- %d::%d. Check your mappings!", t.GVR(), c, len(h))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
|
if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -291,7 +267,7 @@ func (t *Table) buildRow(r int, re render.RowEvent, h render.Header, pads MaxyPa
|
||||||
cell := tview.NewTableCell(field)
|
cell := tview.NewTableCell(field)
|
||||||
cell.SetExpansion(1)
|
cell.SetExpansion(1)
|
||||||
cell.SetAlign(h[c].Align)
|
cell.SetAlign(h[c].Align)
|
||||||
cell.SetTextColor(color(t.GetModel().GetNamespace(), h, re))
|
cell.SetTextColor(color(t.GetModel().GetNamespace(), t.header, ore))
|
||||||
if marked {
|
if marked {
|
||||||
cell.SetTextColor(t.styles.Table().MarkColor.Color())
|
cell.SetTextColor(t.styles.Table().MarkColor.Color())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,14 +68,16 @@ func hotKeyActions(r Runner, aa ui.KeyActions) {
|
||||||
}
|
}
|
||||||
aa[key] = ui.NewSharedKeyAction(
|
aa[key] = ui.NewSharedKeyAction(
|
||||||
hk.Description,
|
hk.Description,
|
||||||
gotoCmd(r, "", hk.Command),
|
gotoCmd(r, hk.Command, ""),
|
||||||
false)
|
false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gotoCmd(r Runner, cmd, path string) ui.ActionHandler {
|
func gotoCmd(r Runner, cmd, path string) ui.ActionHandler {
|
||||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
log.Debug().Msgf("YO! %q -- %q", cmd, path)
|
||||||
if err := r.App().gotoResource(cmd, path, true); err != nil {
|
if err := r.App().gotoResource(cmd, path, true); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Command fail")
|
||||||
r.App().Flash().Err(err)
|
r.App().Flash().Err(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ func (a *App) Init(version string, rate int) error {
|
||||||
a.Content.Stack.AddListener(a.Menu())
|
a.Content.Stack.AddListener(a.Menu())
|
||||||
|
|
||||||
a.App.Init()
|
a.App.Init()
|
||||||
|
a.SetInputCapture(a.keyboard)
|
||||||
a.bindKeys()
|
a.bindKeys()
|
||||||
if a.Conn() == nil {
|
if a.Conn() == nil {
|
||||||
return errors.New("No client connection detected")
|
return errors.New("No client connection detected")
|
||||||
|
|
@ -114,6 +115,23 @@ func (a *App) Init(version string, rate int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
key := evt.Key()
|
||||||
|
if key == tcell.KeyRune {
|
||||||
|
if a.CmdBuff().IsActive() && evt.Modifiers() == tcell.ModNone {
|
||||||
|
a.CmdBuff().Add(evt.Rune())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key = ui.AsKey(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k, ok := a.HasAction(key); ok && !a.Content.IsTopDialog() {
|
||||||
|
return k.Action(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) bindKeys() {
|
func (a *App) bindKeys() {
|
||||||
a.AddActions(ui.KeyActions{
|
a.AddActions(ui.KeyActions{
|
||||||
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
|
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,8 @@ func (d *Deploy) bindKeys(aa ui.KeyActions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Deploy) showPods(app *App, model ui.Tabular, gvr, path string) {
|
func (d *Deploy) showPods(app *App, model ui.Tabular, gvr, path string) {
|
||||||
var res dao.Deployment
|
var ddp dao.Deployment
|
||||||
res.Init(app.factory, d.GVR())
|
dp, err := ddp.Load(app.factory, path)
|
||||||
|
|
||||||
dp, err := res.GetInstance(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Flash().Err(err)
|
app.Flash().Err(err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,13 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortFo
|
||||||
okFn(v, path, extractContainer(p1), tunnel)
|
okFn(v, path, extractContainer(p1), tunnel)
|
||||||
})
|
})
|
||||||
f.AddButton("Cancel", func() {
|
f.AddButton("Cancel", func() {
|
||||||
DismissPortForwards(v.App(), pages)
|
DismissPortForwards(v, pages)
|
||||||
})
|
})
|
||||||
|
|
||||||
modal := tview.NewModalForm(fmt.Sprintf("<PortForward on %s>", path), f)
|
modal := tview.NewModalForm(fmt.Sprintf("<PortForward on %s>", path), f)
|
||||||
modal.SetText("Exposed Ports: " + strings.Join(ports, ","))
|
modal.SetText("Exposed Ports: " + strings.Join(ports, ","))
|
||||||
modal.SetDoneFunc(func(_ int, b string) {
|
modal.SetDoneFunc(func(_ int, b string) {
|
||||||
DismissPortForwards(v.App(), pages)
|
DismissPortForwards(v, pages)
|
||||||
})
|
})
|
||||||
|
|
||||||
pages.AddPage(portForwardKey, modal, false, true)
|
pages.AddPage(portForwardKey, modal, false, true)
|
||||||
|
|
@ -63,9 +63,9 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortFo
|
||||||
}
|
}
|
||||||
|
|
||||||
// DismissPortForwards dismiss the port forward dialog.
|
// DismissPortForwards dismiss the port forward dialog.
|
||||||
func DismissPortForwards(app *App, p *ui.Pages) {
|
func DismissPortForwards(v ResourceViewer, p *ui.Pages) {
|
||||||
p.RemovePage(portForwardKey)
|
p.RemovePage(portForwardKey)
|
||||||
app.SetFocus(p.CurrentPage().Item)
|
v.App().SetFocus(p.CurrentPage().Item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward
|
||||||
|
|
||||||
v.App().QueueUpdateDraw(func() {
|
v.App().QueueUpdateDraw(func() {
|
||||||
v.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
|
v.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
|
||||||
DismissPortForwards(v.App(), v.App().Content.Pages)
|
DismissPortForwards(v, v.App().Content.Pages)
|
||||||
})
|
})
|
||||||
|
|
||||||
pf.SetActive(true)
|
pf.SetActive(true)
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
if podIsRunning(p.App().factory, path) {
|
if !podIsRunning(p.App().factory, path) {
|
||||||
p.App().Flash().Errf("%s is not in a running state", path)
|
p.App().Flash().Errf("%s is not in a running state", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +135,7 @@ func (p *Pod) attachCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
if podIsRunning(p.App().factory, path) {
|
if !podIsRunning(p.App().factory, path) {
|
||||||
p.App().Flash().Errf("%s is not in a happy state", path)
|
p.App().Flash().Errf("%s is not in a happy state", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,14 @@
|
||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
v1 "k8s.io/api/apps/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReplicaSet presents a replicaset viewer.
|
// ReplicaSet presents a replicaset viewer.
|
||||||
|
|
@ -49,38 +38,37 @@ func (r *ReplicaSet) bindKeys(aa ui.KeyActions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReplicaSet) showPods(app *App, model ui.Tabular, gvr, path string) {
|
func (r *ReplicaSet) showPods(app *App, model ui.Tabular, gvr, path string) {
|
||||||
o, err := app.factory.Get(r.GVR().String(), path, true, labels.Everything())
|
var drs dao.ReplicaSet
|
||||||
|
rs, err := drs.Load(app.factory, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Flash().Err(err)
|
app.Flash().Err(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var rs appsv1.ReplicaSet
|
|
||||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs)
|
|
||||||
if err != nil {
|
|
||||||
app.Flash().Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
showPodsFromSelector(app, path, rs.Spec.Selector)
|
showPodsFromSelector(app, path, rs.Spec.Selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReplicaSet) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (r *ReplicaSet) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
sel := r.GetTable().GetSelectedItem()
|
path := r.GetTable().GetSelectedItem()
|
||||||
if sel == "" {
|
if path == "" {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
r.showModal(fmt.Sprintf("Rollback %s %s?", r.GVR(), sel), func(_ int, button string) {
|
r.showModal(fmt.Sprintf("Rollback %s %s?", r.GVR(), path), func(_ int, button string) {
|
||||||
if button == "OK" {
|
defer r.dismissModal()
|
||||||
r.App().Flash().Infof("Rolling back %s %s", r.GVR(), sel)
|
|
||||||
if res, err := rollback(r.App().factory, sel); err != nil {
|
if button != "OK" {
|
||||||
r.App().Flash().Err(err)
|
return
|
||||||
} else {
|
|
||||||
r.App().Flash().Info(res)
|
|
||||||
}
|
|
||||||
r.Refresh()
|
|
||||||
}
|
}
|
||||||
r.dismissModal()
|
r.App().Flash().Infof("Rolling back %s %s", r.GVR(), path)
|
||||||
|
var drs dao.ReplicaSet
|
||||||
|
drs.Init(r.App().factory, r.GVR())
|
||||||
|
if err := drs.Rollback(path); err != nil {
|
||||||
|
r.App().Flash().Err(err)
|
||||||
|
} else {
|
||||||
|
r.App().Flash().Infof("%s successfully rolled back", path)
|
||||||
|
}
|
||||||
|
r.Refresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -99,95 +87,3 @@ func (r *ReplicaSet) showModal(msg string, done func(int, string)) {
|
||||||
r.App().Content.AddPage("confirm", confirm, false, false)
|
r.App().Content.AddPage("confirm", confirm, false, false)
|
||||||
r.App().Content.ShowPage("confirm")
|
r.App().Content.ShowPage("confirm")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Helpers...
|
|
||||||
|
|
||||||
func findRS(f *watch.Factory, path string) (*v1.ReplicaSet, error) {
|
|
||||||
o, err := f.Get("apps/v1/replicasets", path, true, labels.Everything())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var rs appsv1.ReplicaSet
|
|
||||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &rs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findDP(f *watch.Factory, path string) (*appsv1.Deployment, error) {
|
|
||||||
o, err := f.Get("apps/v1/deployments", path, true, labels.Everything())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var dp appsv1.Deployment
|
|
||||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func controllerInfo(rs *v1.ReplicaSet) (string, string, string, error) {
|
|
||||||
for _, ref := range rs.ObjectMeta.OwnerReferences {
|
|
||||||
if ref.Controller == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Debug().Msgf("Controller name %s", ref.Name)
|
|
||||||
tokens := strings.Split(ref.APIVersion, "/")
|
|
||||||
apiGroup := ref.APIVersion
|
|
||||||
if len(tokens) == 2 {
|
|
||||||
apiGroup = tokens[0]
|
|
||||||
}
|
|
||||||
return ref.Name, ref.Kind, apiGroup, nil
|
|
||||||
}
|
|
||||||
return "", "", "", fmt.Errorf("Unable to find controller for ReplicaSet %s", rs.ObjectMeta.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRevision(rs *v1.ReplicaSet) (int64, error) {
|
|
||||||
revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"]
|
|
||||||
if rs.Status.Replicas != 0 {
|
|
||||||
return 0, errors.New("can not rollback current replica")
|
|
||||||
}
|
|
||||||
vers, err := strconv.Atoi(revision)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.New("revision conversion failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return int64(vers), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func rollback(f *watch.Factory, path string) (string, error) {
|
|
||||||
rs, err := findRS(f, path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
version, err := getRevision(rs)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
name, kind, apiGroup, err := controllerInfo(rs)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{Group: apiGroup, Kind: kind}, f.Client().DialOrDie())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
dp, err := findDP(f, client.FQN(rs.Namespace, name))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
res, err := rb.Rollback(dp, map[string]string{}, version, false)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,38 @@ func (t *Table) Init(ctx context.Context) (err error) {
|
||||||
ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles)
|
ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles)
|
||||||
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView)
|
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView)
|
||||||
t.Table.Init(ctx)
|
t.Table.Init(ctx)
|
||||||
|
t.SetInputCapture(t.keyboard)
|
||||||
t.bindKeys()
|
t.bindKeys()
|
||||||
t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second)
|
t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendKey sends an keyboard event (testing only!).
|
||||||
|
func (t *Table) SendKey(evt *tcell.EventKey) {
|
||||||
|
t.keyboard(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
key := evt.Key()
|
||||||
|
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == tcell.KeyRune {
|
||||||
|
if t.FilterInput(evt.Rune()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key = ui.AsKey(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, ok := t.Actions()[key]; ok && !t.app.Content.IsTopDialog() {
|
||||||
|
return a.Action(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
// Name returns the table name.
|
// Name returns the table name.
|
||||||
func (t *Table) Name() string { return t.GVR().R() }
|
func (t *Table) Name() string { return t.GVR().R() }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -78,11 +77,6 @@ func TestTableViewSort(t *testing.T) {
|
||||||
v.SetModel(&testTableModel{})
|
v.SetModel(&testTableModel{})
|
||||||
v.SortColCmd("NAME", true)(nil)
|
v.SortColCmd("NAME", true)(nil)
|
||||||
assert.Equal(t, 3, v.GetRowCount())
|
assert.Equal(t, 3, v.GetRowCount())
|
||||||
for i := 0; i < v.GetRowCount(); i++ {
|
|
||||||
for j := 0; j < v.GetColumnCount(); j++ {
|
|
||||||
fmt.Println(v.GetCell(i, j).Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.Equal(t, "blee", v.GetCell(1, 0).Text)
|
assert.Equal(t, "blee", v.GetCell(1, 0).Text)
|
||||||
|
|
||||||
v.SortInvertCmd(nil)
|
v.SortInvertCmd(nil)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue