checkpoint

mine
derailed 2020-03-28 18:27:17 -06:00
parent 2adafc0133
commit bf2847bea7
54 changed files with 1817 additions and 592 deletions

View File

@ -79,7 +79,7 @@ linters-settings:
# exclude: /path/to/file.txt
funlen:
lines: 65
lines: 75
statements: 40
govet:

View File

@ -19,6 +19,8 @@ builds:
- arm
goarm:
- 7
flags:
- -trimpath
ldflags:
- -s -w -X github.com/derailed/k9s/cmd.version={{.Version}} -X github.com/derailed/k9s/cmd.commit={{.Commit}} -X github.com/derailed/k9s/cmd.date={{.Date}}
archives:

64
go.mod
View File

@ -2,66 +2,40 @@ module github.com/derailed/k9s
go 1.13
replace (
github.com/docker/docker => github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655
k8s.io/apiserver => k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190918162238-f783a3654da8
k8s.io/client-go => k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90
k8s.io/cloud-provider => k8s.io/cloud-provider v0.0.0-20190918163234-a9c1f33e9fb9
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.0.0-20190918163108-da9fdfce26bb
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269
k8s.io/component-base => k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090
k8s.io/cri-api => k8s.io/cri-api v0.0.0-20190828162817-608eb1dad4ac
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.0.0-20190918163402-db86a8c7bb21
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.0.0-20190918161219-8c8f079fddc3
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.0.0-20190918162944-7a93a0ddadd8
k8s.io/kube-proxy => k8s.io/kube-proxy v0.0.0-20190918162534-de037b596c1e
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.0.0-20190918162820-3b5c1246eb18
k8s.io/kubectl => k8s.io/kubectl v0.0.0-20190918164019-21692a0861df
k8s.io/kubelet => k8s.io/kubelet v0.0.0-20190918162654-250a1838aa2c
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.0.0-20190918163543-cfa506e53441
k8s.io/metrics => k8s.io/metrics v0.0.0-20190918162108-227c654b2546
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.0.0-20190918161442-d4c9c65c82af
)
replace github.com/derailed/popeye => /Users/fernand/go_wk/derailed/src/github.com/derailed/popeye
require (
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/atotto/clipboard v0.1.2
github.com/derailed/popeye v0.0.0-00010101000000-000000000000
github.com/derailed/tview v0.3.9
github.com/drone/envsubst v1.0.2 // indirect
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
github.com/fatih/color v1.6.0
github.com/fatih/color v1.9.0
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell v1.3.0
github.com/ghodss/yaml v1.0.0
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
github.com/mattn/go-runewidth v0.0.8
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.9
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
github.com/openfaas/faas-provider v0.15.0
github.com/petergtz/pegomock v2.6.0+incompatible
github.com/rakyll/hey v0.1.2
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1
github.com/petergtz/pegomock v2.7.0+incompatible
github.com/rakyll/hey v0.1.3
github.com/rs/zerolog v1.18.0
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sahilm/fuzzy v0.1.0
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
github.com/spf13/cobra v0.0.6
github.com/stretchr/testify v1.5.1
golang.org/x/text v0.3.2
gopkg.in/yaml.v2 v2.2.4
helm.sh/helm/v3 v3.0.2
k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0
k8s.io/cli-runtime v0.0.0
k8s.io/client-go v0.0.0
gopkg.in/yaml.v2 v2.2.8
helm.sh/helm/v3 v3.1.2
k8s.io/api v0.18.0
k8s.io/apimachinery v0.18.0
k8s.io/cli-runtime v0.18.0
k8s.io/client-go v0.18.0
k8s.io/klog v1.0.0
k8s.io/kubectl v0.0.0
k8s.io/kubernetes v1.16.3
k8s.io/metrics v0.0.0
sigs.k8s.io/yaml v1.1.0
k8s.io/kubectl v0.18.0
k8s.io/metrics v0.18.0
rsc.io/letsencrypt v0.0.3 // indirect
sigs.k8s.io/yaml v1.2.0
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787
)

619
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package client
import (
"context"
"fmt"
"path/filepath"
"strings"
@ -27,6 +28,7 @@ const (
cacheMXKey = "metrics"
cacheMXAPIKey = "metricsAPI"
checkConnTimeout = 10 * time.Second
CallTimeout = 5 * time.Second
)
var supportedMetricsAPIVersions = []string{"v1beta1"}
@ -86,6 +88,33 @@ func makeCacheKey(ns, gvr string, vv []string) string {
return ns + ":" + gvr + "::" + strings.Join(vv, ",")
}
// ActiveCluster returns the current cluster name.
func (a *APIClient) ActiveCluster() string {
c, err := a.config.CurrentClusterName()
if err != nil {
log.Error().Msgf("Unable to located active cluster")
return ""
}
return c
}
// IsActiveNamespace returns true if namespaces matches.
func (a *APIClient) IsActiveNamespace(ns string) bool {
if a.ActiveNamespace() == AllNamespaces {
return true
}
return a.ActiveNamespace() == ns
}
// ActiveNamespace returns the current namespace.
func (a *APIClient) ActiveNamespace() string {
ns, err := a.CurrentNamespaceName()
if err != nil {
return AllNamespaces
}
return ns
}
func (a *APIClient) clearCache() {
for _, k := range a.cache.Keys() {
a.cache.Remove(k)
@ -104,9 +133,12 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error)
}
}
dial, sar := a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr)
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
defer cancel()
for _, v := range verbs {
sar.Spec.ResourceAttributes.Verb = v
resp, err := dial.Create(sar)
resp, err := dial.Create(ctx, sar, metav1.CreateOptions{})
if err != nil {
log.Warn().Err(err).Msgf(" Dial Failed!")
a.cache.Add(key, false, cacheExpiry)
@ -135,7 +167,9 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
// ValidNamespaces returns all available namespaces.
func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
nn, err := a.DialOrDie().CoreV1().Namespaces().List(metav1.ListOptions{})
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
defer cancel()
nn, err := a.DialOrDie().CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
@ -197,7 +231,9 @@ func (a *APIClient) HasMetrics() bool {
a.cache.Add(cacheMXKey, flag, cacheExpiry)
return flag
}
if _, err := dial.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{Limit: 1}); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
defer cancel()
if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil {
flag = true
}
a.cache.Add(cacheMXKey, flag, cacheExpiry)

View File

@ -1,6 +1,7 @@
package client
import (
"context"
"fmt"
"math"
"time"
@ -129,7 +130,7 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
}
// FetchNodesMetrics return all metrics for nodes.
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error) {
const msg = "user is not authorized to list node metrics"
mx := new(mv1beta1.NodeMetricsList)
@ -150,7 +151,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
if err != nil {
return mx, err
}
mxList, err := client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
mxList, err := client.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{})
if err != nil {
return mx, err
}
@ -160,7 +161,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
}
// FetchPodsMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error) {
mx := new(mv1beta1.PodMetricsList)
const msg = "user is not authorized to list pods metrics"
@ -184,7 +185,7 @@ func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, e
if err != nil {
return mx, err
}
mxList, err := client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
mxList, err := client.MetricsV1beta1().PodMetricses(ns).List(ctx, metav1.ListOptions{})
if err != nil {
return mx, err
}
@ -194,7 +195,7 @@ func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, e
}
// FetchPodMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1beta1.PodMetrics, error) {
var mx *mv1beta1.PodMetrics
const msg = "user is not authorized to list pod metrics"
@ -218,7 +219,7 @@ func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error
if err != nil {
return mx, err
}
mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{})
mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(ctx, n, metav1.GetOptions{})
if err != nil {
return mx, err
}

View File

@ -101,6 +101,15 @@ type Connection interface {
// CheckConnectivity checks if api server connection is happy or not.
CheckConnectivity() bool
// ActiveCluster returns the current cluster name.
ActiveCluster() string
// ActiveNamespace returns the current namespace.
ActiveNamespace() string
// IsActiveNamespace checks if given ns is active.
IsActiveNamespace(string) bool
}
// CurrentMetrics tracks current cpu/mem.

View File

@ -2,8 +2,6 @@ package color
import (
"fmt"
"github.com/rs/zerolog/log"
)
// ColorFmt colorize a string with ansi colors.
@ -29,7 +27,6 @@ const (
// Colorize returns an ASCII colored string based on given color.
func Colorize(s string, c Paint) string {
log.Debug().Msgf("Painting %#v", c)
if c == 0 {
return s
}

View File

@ -142,6 +142,8 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("help", "h", "?")
a.declare("quit", "q", "Q")
a.declare("aliases", "alias", "a")
a.declare("popeye", "pop")
a.declare("sanitize", "san", "sanitize")
a.declare("contexts", "context", "ctx")
a.declare("users", "user", "usr")
a.declare("groups", "group", "grp")

View File

@ -142,6 +142,51 @@ func (mock *MockConnection) HasMetrics() bool {
return ret0
}
func (mock *MockConnection) ActiveCluster() string {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveCluster", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})
var ret0 string
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
}
return ret0
}
func (mock *MockConnection) ActiveNamespace() string {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveNamespace", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})
var ret0 string
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
}
return ret0
}
func (mock *MockConnection) IsActiveNamespace(s string) bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("IsActiveNamespace", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})
var ret0 bool
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(bool)
}
}
return ret0
}
func (mock *MockConnection) IsNamespaced(_param0 string) bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")

View File

@ -1,123 +1,124 @@
package dao
import (
"context"
"fmt"
"os"
// BOZO!! v1.18.0
// import (
// "context"
// "fmt"
// "os"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/action"
"k8s.io/apimachinery/pkg/runtime"
)
// "github.com/derailed/k9s/internal/client"
// "github.com/derailed/k9s/internal/render"
// "github.com/rs/zerolog/log"
// "helm.sh/helm/v3/pkg/action"
// "k8s.io/apimachinery/pkg/runtime"
// )
var (
_ Accessor = (*Chart)(nil)
_ Nuker = (*Chart)(nil)
_ Describer = (*Chart)(nil)
)
// var (
// _ Accessor = (*Chart)(nil)
// _ Nuker = (*Chart)(nil)
// _ Describer = (*Chart)(nil)
// )
// Chart represents a helm chart.
type Chart struct {
NonResource
}
// // Chart represents a helm chart.
// type Chart struct {
// NonResource
// }
// List returns a collection of resources.
func (c *Chart) List(ctx context.Context, ns string) ([]runtime.Object, error) {
cfg, err := c.EnsureHelmConfig(ns)
if err != nil {
return nil, err
}
// // List returns a collection of resources.
// func (c *Chart) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// cfg, err := c.EnsureHelmConfig(ns)
// if err != nil {
// return nil, err
// }
rr, err := action.NewList(cfg).Run()
if err != nil {
return nil, err
}
// rr, err := action.NewList(cfg).Run()
// if err != nil {
// return nil, err
// }
oo := make([]runtime.Object, 0, len(rr))
for _, r := range rr {
oo = append(oo, render.ChartRes{Release: r})
}
// oo := make([]runtime.Object, 0, len(rr))
// for _, r := range rr {
// oo = append(oo, render.ChartRes{Release: r})
// }
return oo, nil
}
// return oo, nil
// }
// Get returns a resource.
func (c *Chart) Get(_ context.Context, path string) (runtime.Object, error) {
ns, n := client.Namespaced(path)
cfg, err := c.EnsureHelmConfig(ns)
if err != nil {
return nil, err
}
resp, err := action.NewGet(cfg).Run(n)
if err != nil {
return nil, err
}
// // Get returns a resource.
// func (c *Chart) Get(_ context.Context, path string) (runtime.Object, error) {
// ns, n := client.Namespaced(path)
// cfg, err := c.EnsureHelmConfig(ns)
// if err != nil {
// return nil, err
// }
// resp, err := action.NewGet(cfg).Run(n)
// if err != nil {
// return nil, err
// }
return render.ChartRes{Release: resp}, nil
}
// return render.ChartRes{Release: resp}, nil
// }
// Describe returns the chart notes.
func (c *Chart) Describe(path string) (string, error) {
ns, n := client.Namespaced(path)
cfg, err := c.EnsureHelmConfig(ns)
if err != nil {
return "", err
}
resp, err := action.NewGet(cfg).Run(n)
if err != nil {
return "", err
}
// // Describe returns the chart notes.
// func (c *Chart) Describe(path string) (string, error) {
// ns, n := client.Namespaced(path)
// cfg, err := c.EnsureHelmConfig(ns)
// if err != nil {
// return "", err
// }
// resp, err := action.NewGet(cfg).Run(n)
// if err != nil {
// return "", err
// }
return resp.Info.Notes, nil
}
// return resp.Info.Notes, nil
// }
// ToYAML returns the chart manifest.
func (c *Chart) ToYAML(path string) (string, error) {
ns, n := client.Namespaced(path)
cfg, err := c.EnsureHelmConfig(ns)
if err != nil {
return "", err
}
resp, err := action.NewGet(cfg).Run(n)
if err != nil {
return "", err
}
// // ToYAML returns the chart manifest.
// func (c *Chart) ToYAML(path string) (string, error) {
// ns, n := client.Namespaced(path)
// cfg, err := c.EnsureHelmConfig(ns)
// if err != nil {
// return "", err
// }
// resp, err := action.NewGet(cfg).Run(n)
// if err != nil {
// return "", err
// }
return resp.Manifest, nil
}
// return resp.Manifest, nil
// }
// Delete uninstall a Chart.
func (c *Chart) Delete(path string, cascade, force bool) error {
ns, n := client.Namespaced(path)
cfg, err := c.EnsureHelmConfig(ns)
if err != nil {
return err
}
// // Delete uninstall a Chart.
// func (c *Chart) Delete(path string, cascade, force bool) error {
// ns, n := client.Namespaced(path)
// cfg, err := c.EnsureHelmConfig(ns)
// if err != nil {
// return err
// }
res, err := action.NewUninstall(cfg).Run(n)
if err != nil {
return err
}
// res, err := action.NewUninstall(cfg).Run(n)
// if err != nil {
// return err
// }
if res != nil && res.Info != "" {
return fmt.Errorf("%s", res.Info)
}
// if res != nil && res.Info != "" {
// return fmt.Errorf("%s", res.Info)
// }
return nil
}
// return nil
// }
// EnsureHelmConfig return a new configuration.
func (c *Chart) EnsureHelmConfig(ns string) (*action.Configuration, error) {
cfg := new(action.Configuration)
flags := c.Client().Config().Flags()
if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
return nil, err
}
return cfg, nil
}
// // EnsureHelmConfig return a new configuration.
// func (c *Chart) EnsureHelmConfig(ns string) (*action.Configuration, error) {
// cfg := new(action.Configuration)
// flags := c.Client().Config().Flags()
// if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
// return nil, err
// }
// return cfg, nil
// }
func helmLogger(s string, args ...interface{}) {
log.Debug().Msgf("%s %v", s, args)
}
// func helmLogger(s string, args ...interface{}) {
// log.Debug().Msgf("%s %v", s, args)
// }

View File

@ -37,7 +37,7 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
err error
)
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(fqn); err != nil {
if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(ctx, fqn); err != nil {
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
}
}

View File

@ -60,6 +60,9 @@ func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error)
func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil }
func (c *conn) CurrentNamespaceName() (string, error) { return "", nil }
func (c *conn) CanI(ns, gvr string, verbs []string) (bool, error) { return true, nil }
func (c *conn) ActiveCluster() string { return "" }
func (c *conn) ActiveNamespace() string { return "" }
func (c *conn) IsActiveNamespace(string) bool { return false }
type podFactory struct{}

View File

@ -1,6 +1,7 @@
package dao
import (
"context"
"fmt"
"github.com/derailed/k9s/internal/client"
@ -33,7 +34,9 @@ func (c *CronJob) Run(path string) error {
}
// BOZO!! Factory resource??
cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(n, metav1.GetOptions{})
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(ctx, n, metav1.GetOptions{})
if err != nil {
return err
}
@ -51,7 +54,7 @@ func (c *CronJob) Run(path string) error {
},
Spec: cj.Spec.JobTemplate.Spec,
}
_, err = c.Client().DialOrDie().BatchV1().Jobs(ns).Create(job)
_, err = c.Client().DialOrDie().BatchV1().Jobs(ns).Create(ctx, job, metav1.CreateOptions{})
return err
}

View File

@ -4,7 +4,6 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
"k8s.io/kubectl/pkg/describe"
"k8s.io/kubectl/pkg/describe/versioned"
)
// Describe describes a resource.
@ -31,7 +30,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error)
log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, n)
return "", err
}
d, err := versioned.Describer(c.Config().Flags(), mapping)
d, err := describe.Describer(c.Config().Flags(), mapping)
if err != nil {
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
return "", err

View File

@ -35,7 +35,7 @@ func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
}
// Scale a Deployment.
func (d *Deployment) Scale(path string, replicas int32) error {
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path)
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb})
if err != nil {
@ -45,18 +45,18 @@ func (d *Deployment) Scale(path string, replicas int32) error {
return fmt.Errorf("user is not authorized to scale a deployment")
}
scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{})
scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(ctx, n, metav1.GetOptions{})
if err != nil {
return err
}
scale.Spec.Replicas = replicas
_, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale)
_, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{})
return err
}
// Restart a Deployment rollout.
func (d *Deployment) Restart(path string) error {
func (d *Deployment) Restart(ctx context.Context, path string) error {
dp, err := d.Load(d.Factory, path)
if err != nil {
return err
@ -75,7 +75,13 @@ func (d *Deployment) Restart(path string) error {
return err
}
_, err = d.Client().DialOrDie().AppsV1().Deployments(dp.Namespace).Patch(dp.Name, types.StrategicMergePatchType, update)
_, err = d.Client().DialOrDie().AppsV1().Deployments(dp.Namespace).Patch(
ctx,
dp.Name,
types.StrategicMergePatchType,
update,
metav1.PatchOptions{},
)
return err
}

View File

@ -38,7 +38,7 @@ func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool {
}
// Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(path string) error {
func (d *DaemonSet) Restart(ctx context.Context, path string) error {
ds, err := d.GetInstance(path)
if err != nil {
return err
@ -56,7 +56,13 @@ func (d *DaemonSet) Restart(path string) error {
return err
}
_, err = d.Client().DialOrDie().AppsV1().DaemonSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update)
_, err = d.Client().DialOrDie().AppsV1().DaemonSets(ds.Namespace).Patch(
ctx,
ds.Name,
types.StrategicMergePatchType,
update,
metav1.PatchOptions{},
)
return err
}

View File

@ -39,9 +39,9 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error)
err error
)
if client.IsClusterScoped(ns) {
ll, err = g.dynClient().List(metav1.ListOptions{LabelSelector: labelSel})
ll, err = g.dynClient().List(ctx, metav1.ListOptions{LabelSelector: labelSel})
} else {
ll, err = g.dynClient().Namespace(ns).List(metav1.ListOptions{LabelSelector: labelSel})
ll, err = g.dynClient().Namespace(ns).List(ctx, metav1.ListOptions{LabelSelector: labelSel})
}
if err != nil {
return nil, err
@ -57,15 +57,15 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error)
// Get returns a given resource.
func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) {
log.Debug().Msgf("GENERIC-GET %q", path)
var opts metav1.GetOptions
ns, n := client.Namespaced(path)
dial := g.dynClient()
if client.IsClusterScoped(ns) {
return dial.Get(n, opts)
return dial.Get(ctx, n, opts)
}
return dial.Namespace(ns).Get(n, opts)
return dial.Namespace(ns).Get(ctx, n, opts)
}
// Describe describes a resource.
@ -111,11 +111,14 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
PropagationPolicy: &p,
GracePeriodSeconds: grace,
}
// BOZO!! Move to caller!
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
if client.IsClusterScoped(ns) {
return g.dynClient().Delete(n, &opts)
return g.dynClient().Delete(ctx, n, opts)
}
return g.dynClient().Namespace(ns).Delete(n, &opts)
return g.dynClient().Namespace(ns).Delete(ctx, n, opts)
}
func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface {

View File

@ -50,7 +50,7 @@ func (n *Node) ToggleCordon(path string, cordon bool) error {
}
return fmt.Errorf("node is already uncordoned")
}
err, patchErr := h.PatchOrReplace(n.Factory.Client().DialOrDie())
err, patchErr := h.PatchOrReplace(n.Factory.Client().DialOrDie(), false)
if patchErr != nil {
return patchErr
}
@ -97,8 +97,30 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
}
// Get returns a node resource.
func (n *Node) Get(_ context.Context, path string) (runtime.Object, error) {
return FetchNode(n.Factory, path)
func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) {
var (
nmx *mv1beta1.NodeMetricsList
err error
)
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx); err != nil {
log.Warn().Err(err).Msgf("No node metrics")
}
}
no, err := FetchNode(ctx, n.Factory, path)
if err != nil {
return nil, err
}
o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&no)
if err != nil {
return nil, err
}
return &render.NodeWithMetrics{
Raw: &unstructured.Unstructured{Object: o},
MX: nodeMetricsFor(MetaFQN(no.ObjectMeta), nmx),
}, nil
}
// List returns a collection of node resources.
@ -113,12 +135,12 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
err error
)
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(); err != nil {
if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx); err != nil {
log.Warn().Err(err).Msgf("No node metrics")
}
}
nn, err := FetchNodes(n.Factory, labels)
nn, err := FetchNodes(ctx, n.Factory, labels)
if err != nil {
return nil, err
}
@ -141,7 +163,7 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// Helpers...
// FetchNode retrieves a node.
func FetchNode(f Factory, path string) (*v1.Node, error) {
func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
auth, err := f.Client().CanI("", "v1/nodes", []string{"get"})
if err != nil {
return nil, err
@ -150,11 +172,11 @@ func FetchNode(f Factory, path string) (*v1.Node, error) {
return nil, fmt.Errorf("user is not authorized to list nodes")
}
return f.Client().DialOrDie().CoreV1().Nodes().Get(path, metav1.GetOptions{})
return f.Client().DialOrDie().CoreV1().Nodes().Get(ctx, path, metav1.GetOptions{})
}
// FetchNodes retrieves all nodes.
func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) {
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
if err != nil {
return nil, err
@ -163,7 +185,7 @@ func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
return nil, fmt.Errorf("user is not authorized to list nodes")
}
return f.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{
return f.Client().DialOrDie().CoreV1().Nodes().List(ctx, metav1.ListOptions{
LabelSelector: labelsSel,
})
}

View File

@ -57,7 +57,7 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
var pmx *mv1beta1.PodMetrics
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(ctx, path); err != nil {
log.Debug().Err(err).Msgf("No pod metrics")
}
}
@ -84,7 +84,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
var pmx *mv1beta1.PodMetricsList
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ctx, ns); err != nil {
log.Debug().Err(err).Msgf("No pods metrics")
}
}
@ -235,10 +235,9 @@ func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) er
if err != nil {
return err
}
req.Context(ctx)
// This call will block if nothing is in the stream!!
stream, err := req.Stream()
stream, err := req.Stream(ctx)
if err != nil {
c <- opts.DecorateLog([]byte(err.Error() + "\n"))
log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path)
@ -263,11 +262,11 @@ func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) {
if err != nil {
if err == io.EOF {
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info())
c <- NewLogItemFromString("<STREAM> closed")
c <- opts.DecorateLog([]byte("log stream closed\n"))
return
}
log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info())
c <- NewLogItemFromString("<STREAM> failed")
c <- opts.DecorateLog([]byte("log stream failed\n"))
return
}
c <- opts.DecorateLog(bytes)

131
internal/dao/popeye.go Normal file
View File

@ -0,0 +1,131 @@
package dao
import (
"bytes"
"context"
"encoding/json"
"errors"
"path/filepath"
"sort"
"time"
"github.com/derailed/k9s/internal/client"
cfg "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/popeye/pkg"
"github.com/derailed/popeye/pkg/config"
"github.com/derailed/popeye/types"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
restclient "k8s.io/client-go/rest"
)
var _ Accessor = (*Popeye)(nil)
// Popeye tracks cluster sanitization.
type Popeye struct {
NonResource
}
// NewPopeye returns a new set of aliases.
func NewPopeye(f Factory) *Popeye {
a := Popeye{}
a.Init(f, client.NewGVR("popeye"))
return &a
}
type readWriteCloser struct {
*bytes.Buffer
}
// Close close read stream.
func (readWriteCloser) Close() error {
return nil
}
// List returns a collection of aliases.
func (p *Popeye) List(ctx context.Context, _ string) ([]runtime.Object, error) {
defer func(t time.Time) {
log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
}(time.Now())
js := "json"
flags := config.NewFlags()
spinach := filepath.Join(cfg.K9sHome, "spinach.yml")
flags.Spinach = &spinach
flags.Output = &js
popeye, err := pkg.NewPopeye(flags, &log.Logger)
if err != nil {
return nil, err
}
popeye.SetFactory(newPopFactory(p.Factory))
if err = popeye.Init(); err != nil {
return nil, err
}
buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
popeye.SetOutputTarget(buff)
if err = popeye.Sanitize(); err != nil {
return nil, err
}
var b render.Builder
if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
return nil, err
}
oo := make([]runtime.Object, 0, len(b.Report.Sections))
sort.Sort(b.Report.Sections)
for _, s := range b.Report.Sections {
s.Tally.Count = len(s.Outcome)
if s.Tally.Sum() > 0 {
oo = append(oo, s)
}
}
return oo, nil
}
// Get fetch a resource.
func (a *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!")
}
type popFactory struct {
Factory
}
var _ types.Factory = (*popFactory)(nil)
func newPopFactory(f Factory) *popFactory {
return &popFactory{Factory: f}
}
func (p *popFactory) Client() types.Connection {
return &popConnection{Connection: p.Factory.Client()}
}
type popConnection struct {
client.Connection
}
var _ types.Connection = (*popConnection)(nil)
func (c *popConnection) Config() types.Config {
return c.Connection.Config()
}
func (c *popConnection) CurrentNamespaceName() (string, error) {
return c.ActiveNamespace(), nil
}
func (c *popConnection) CurrentClusterName() (string, error) {
return c.Connection.ActiveCluster(), nil
}
func (c *popConnection) Flags() *genericclioptions.ConfigFlags {
return c.Connection.Config().Flags()
}
func (c *popConnection) RESTConfig() (*restclient.Config, error) {
return c.Connection.Config().RESTConfig()
}

View File

@ -7,6 +7,7 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
@ -119,6 +120,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
}
func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) {
log.Debug().Msgf("LOAD-CR %q", path)
o, err := r.Factory.Get(crGVR, path, true, labels.Everything())
if err != nil {
return nil, err

View File

@ -47,8 +47,11 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
client.NewGVR("batch/v1/jobs"): &Job{},
client.NewGVR("charts"): &Chart{},
client.NewGVR("openfaas"): &OpenFaas{},
// BOZO!! v1.18.0
// client.NewGVR("charts"): &Chart{},
client.NewGVR("openfaas"): &OpenFaas{},
client.NewGVR("popeye"): &Popeye{},
client.NewGVR("report"): &Sanitizer{},
}
r, ok := m[gvr]
@ -163,6 +166,20 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{},
Categories: []string{"k9s"},
}
m[client.NewGVR("popeye")] = metav1.APIResource{
Name: "popeye",
Kind: "Popeye",
SingularName: "popeye",
Verbs: []string{},
Categories: []string{"k9s"},
}
m[client.NewGVR("report")] = metav1.APIResource{
Name: "report",
Kind: "Report",
SingularName: "report",
Verbs: []string{},
Categories: []string{"k9s"},
}
m[client.NewGVR("contexts")] = metav1.APIResource{
Name: "contexts",
Kind: "Contexts",

View File

@ -13,6 +13,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/polymorphichelpers"
)
@ -94,7 +95,7 @@ func (r *ReplicaSet) Rollback(fqn string) error {
return err
}
_, err = rb.Rollback(dp, map[string]string{}, version, false)
_, err = rb.Rollback(dp, map[string]string{}, version, cmdutil.DryRunNone)
if err != nil {
return err
}

80
internal/dao/sanitizer.go Normal file
View File

@ -0,0 +1,80 @@
package dao
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
cfg "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/popeye/pkg"
"github.com/derailed/popeye/pkg/config"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
)
var _ Accessor = (*Sanitizer)(nil)
// Sanitizer tracks cluster sanitization.
type Sanitizer struct {
NonResource
}
// NewSanitizer returns a new set of aliases.
func NewSanitizer(f Factory) *Sanitizer {
s := Sanitizer{}
s.Init(f, client.NewGVR("report"))
return &s
}
// List returns a collection of aliases.
func (s *Sanitizer) List(ctx context.Context, _ string) ([]runtime.Object, error) {
report, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, fmt.Errorf("no sanitizer report path")
}
sections := []string{report}
js := "json"
flags := config.NewFlags()
flags.Sections = &sections
spinach := filepath.Join(cfg.K9sHome, "spinach.yml")
flags.Spinach = &spinach
flags.Output = &js
popeye, err := pkg.NewPopeye(flags, &log.Logger)
if err != nil {
return nil, err
}
popeye.SetFactory(newPopFactory(s.Factory))
if err = popeye.Init(); err != nil {
return nil, err
}
buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
popeye.SetOutputTarget(buff)
if err = popeye.Sanitize(); err != nil {
return nil, err
}
var b render.Builder
if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
return nil, err
}
oo := make([]runtime.Object, len(b.Report.Sections))
for i, s := range b.Report.Sections {
oo[i] = s
}
return oo, nil
}
// Get fetch a resource.
func (*Sanitizer) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!")
}

View File

@ -35,7 +35,7 @@ func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
}
// Scale a StatefulSet.
func (s *StatefulSet) Scale(path string, replicas int32) error {
func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path)
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb})
if err != nil {
@ -45,18 +45,18 @@ func (s *StatefulSet) Scale(path string, replicas int32) error {
return fmt.Errorf("user is not authorized to scale statefulsets")
}
scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{})
scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(ctx, n, metav1.GetOptions{})
if err != nil {
return err
}
scale.Spec.Replicas = replicas
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale)
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{})
return err
}
// Restart a StatefulSet rollout.
func (s *StatefulSet) Restart(path string) error {
func (s *StatefulSet) Restart(ctx context.Context, path string) error {
sts, err := s.getStatefulSet(path)
if err != nil {
return err
@ -76,7 +76,13 @@ func (s *StatefulSet) Restart(path string) error {
return err
}
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(sts.Namespace).Patch(sts.Name, types.StrategicMergePatchType, update)
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(sts.Namespace).Patch(
ctx,
sts.Name,
types.StrategicMergePatchType,
update,
metav1.PatchOptions{},
)
return err
}

View File

@ -21,8 +21,6 @@ type Table struct {
// Get returns a given resource.
func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
ns, n := client.Namespaced(path)
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
_, codec := t.codec()
@ -30,18 +28,17 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
if err != nil {
return nil, err
}
o, err := c.Get().
ns, n := client.Namespaced(path)
req := c.Get().
SetHeader("Accept", a).
Namespace(ns).
Name(n).
Resource(t.gvr.R()).
VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get()
if err != nil {
return nil, err
VersionedParams(&metav1beta1.TableOptions{}, codec)
if ns != client.ClusterScope {
req = req.Namespace(ns)
}
return o, nil
return req.Do(ctx).Get()
}
// List all Resources in a given namespace.
@ -63,7 +60,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
Namespace(ns).
Resource(t.gvr.R()).
VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec).
Do().Get()
Do(ctx).Get()
if err != nil {
return nil, err
}

View File

@ -108,7 +108,7 @@ type Describer interface {
// Scalable represents resources that can scale.
type Scalable interface {
// Scale scales a resource up or down.
Scale(path string, replicas int32) error
Scale(ctx context.Context, path string, replicas int32) error
}
// Controller represents a pod controller.
@ -132,7 +132,7 @@ type Switchable interface {
// Restartable represents a restartable resource.
type Restartable interface {
// Restart performs a rollout restart.
Restart(path string) error
Restart(ctx context.Context, path string) error
}
// Runnable represents a runnable resource.

View File

@ -1,6 +1,8 @@
package model
import (
"context"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
v1 "k8s.io/api/core/v1"
@ -20,8 +22,8 @@ type (
// MetricsService calls the metrics server for metrics info.
MetricsService interface {
HasMetrics() bool
FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error)
FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error)
FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error)
FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error)
}
// Cluster represents a kubernetes resource.
@ -77,13 +79,13 @@ func (c *Cluster) UserName() string {
}
// Metrics gathers node level metrics and compute utilization percentages.
func (c *Cluster) Metrics(mx *client.ClusterMetrics) error {
nn, err := dao.FetchNodes(c.factory, "")
func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error {
nn, err := dao.FetchNodes(ctx, c.factory, "")
if err != nil {
return err
}
nmx, err := c.mx.FetchNodesMetrics()
nmx, err := c.mx.FetchNodesMetrics(ctx)
if err != nil {
return err
}

View File

@ -1,6 +1,8 @@
package model
import (
"context"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
)
@ -90,8 +92,10 @@ func (c *ClusterInfo) Refresh() {
data.K9sVer = c.version
data.K8sVer = c.cluster.Version()
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
var mx client.ClusterMetrics
if err := c.cluster.Metrics(&mx); err == nil {
if err := c.cluster.Metrics(ctx, &mx); err == nil {
data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral
}

View File

@ -176,7 +176,6 @@ func (l *Log) load() error {
if l.cancelFn != nil {
l.cancelFn()
}
close(c)
return err
}

View File

@ -51,7 +51,7 @@ func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, er
hh = append(hh, c)
}
mm, err := h.checkMetrics()
mm, err := h.checkMetrics(ctx)
if err != nil {
return hh, nil
}
@ -62,15 +62,15 @@ func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, er
return hh, nil
}
func (h *PulseHealth) checkMetrics() (health.Checks, error) {
func (h *PulseHealth) checkMetrics(ctx context.Context) (health.Checks, error) {
dial := client.DialMetrics(h.factory.Client())
nn, err := dao.FetchNodes(h.factory, "")
nn, err := dao.FetchNodes(ctx, h.factory, "")
if err != nil {
return nil, err
}
nmx, err := dial.FetchNodesMetrics()
nmx, err := dial.FetchNodesMetrics(ctx)
if err != nil {
log.Error().Err(err).Msgf("Fetching metrics")
return nil, err

View File

@ -10,10 +10,11 @@ import (
// BOZO!! Break up deps and merge into single registrar
var Registry = map[string]ResourceMeta{
// Custom...
"charts": {
DAO: &dao.Chart{},
Renderer: &render.Chart{},
},
// BOZO!! v1.18.0
// "charts": {
// DAO: &dao.Chart{},
// Renderer: &render.Chart{},
// },
"pulses": {
DAO: &dao.Pulse{},
},
@ -62,6 +63,14 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Alias{},
Renderer: &render.Alias{},
},
"popeye": {
DAO: &dao.Popeye{},
Renderer: &render.Popeye{},
},
"report": {
DAO: &dao.Sanitizer{},
TreeRenderer: &xray.Section{},
},
// Core...
"v1/endpoints": {

View File

@ -117,7 +117,6 @@ func (b *Benchmark) Run(cluster string, done func()) {
// this call will block until the benchmark is complete or timesout.
b.worker.Run()
b.worker.Stop()
log.Debug().Msgf("YO!! %t %s", b.canceled, buff)
if len(buff.Bytes()) > 0 {
if err := b.save(cluster, buff); err != nil {
log.Error().Err(err).Msg("Saving Benchmark")

View File

@ -70,7 +70,6 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
if !ok {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
}
r.ID = client.FQN(nns, n)
r.Fields = make(Fields, 0, len(g.Header(ns)))
r.Fields = append(r.Fields, nns)

View File

@ -55,7 +55,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
pdb.Name,
numbToStr(pdb.Spec.MinAvailable),
numbToStr(pdb.Spec.MaxUnavailable),
strconv.Itoa(int(pdb.Status.PodDisruptionsAllowed)),
strconv.Itoa(int(pdb.Status.DisruptionsAllowed)),
strconv.Itoa(int(pdb.Status.CurrentHealthy)),
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
strconv.Itoa(int(pdb.Status.ExpectedPods)),

View File

@ -13,7 +13,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/util/node"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
@ -262,7 +261,7 @@ func (*Pod) Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
func (p *Pod) Phase(po *v1.Pod) string {
status := string(po.Status.Phase)
if po.Status.Reason != "" {
if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason {
if po.DeletionTimestamp != nil && po.Status.Reason == "NodeLost" {
return "Unknown"
}
status = po.Status.Reason

187
internal/render/popeye.go Normal file
View File

@ -0,0 +1,187 @@
package render
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/derailed/popeye/pkg/config"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Popeye renders a sanitizer to screen.
type Popeye struct{}
// ColorerFunc colors a resource row.
func (Popeye) ColorerFunc() ColorerFunc {
return func(ns string, h Header, re RowEvent) tcell.Color {
c := DefaultColorer(ns, h, re)
warnCol := h.IndexOf("WARNING", true)
status, _ := strconv.Atoi(strings.TrimSpace(re.Row.Fields[warnCol]))
if status > 0 {
c = tcell.ColorOrange
}
errCol := h.IndexOf("ERROR", true)
status, _ = strconv.Atoi(strings.TrimSpace(re.Row.Fields[errCol]))
if status > 0 {
c = ErrColor
}
return c
}
}
// Header returns a header row.
func (Popeye) Header(ns string) Header {
return Header{
HeaderColumn{Name: "RESOURCE"},
HeaderColumn{Name: "SCORE%", Align: tview.AlignRight},
HeaderColumn{Name: "SCANNED", Align: tview.AlignRight},
HeaderColumn{Name: "OK", Align: tview.AlignRight},
HeaderColumn{Name: "INFO", Align: tview.AlignRight},
HeaderColumn{Name: "WARNING", Align: tview.AlignRight},
HeaderColumn{Name: "ERROR", Align: tview.AlignRight},
}
}
// Render renders a K8s resource to screen.
func (Popeye) Render(o interface{}, ns string, r *Row) error {
s, ok := o.(Section)
if !ok {
return fmt.Errorf("expected Section, but got %T", o)
}
r.ID = s.Title
r.Fields = append(r.Fields,
s.Title,
strconv.Itoa(s.Tally.Score()),
strconv.Itoa(s.Tally.OK+s.Tally.Info+s.Tally.Warning+s.Tally.Error),
strconv.Itoa(s.Tally.OK),
strconv.Itoa(s.Tally.Info),
strconv.Itoa(s.Tally.Warning),
strconv.Itoa(s.Tally.Error),
)
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
// BOZO!! export!
type (
// Builder represents sanitizer
Builder struct {
Report Report `json:"popeye" yaml:"popeye"`
}
// Report represents the output of a sanitization pass.
Report struct {
Score int `json:"score" yaml:"score"`
Grade string `json:"grade" yaml:"grade"`
Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"`
}
// Sections represents a collection of sections.
Sections []Section
// Section represents a sanitizer pass
Section struct {
Title string `json:"sanitizer" yaml:"sanitizer"`
Tally *Tally `json:"tally" yaml:"tally"`
Outcome Outcome `json:"issues,omitempty" yaml:"issues,omitempty"`
}
Outcome map[string]Issues
Issues []Issue
Issue struct {
Group string `yaml:"group" json:"group"`
Level config.Level `yaml:"level" json:"level"`
Message string `yaml:"message" json:"message"`
}
Tally struct {
OK, Info, Warning, Error int
Count int
}
)
func (t *Tally) Sum() int {
return t.OK + t.Info + t.Warning + t.Error
}
func (t *Tally) Score() int {
oks := t.OK + t.Info
return toPerc(float64(oks), float64(oks+t.Warning+t.Error))
}
func toPerc(v1, v2 float64) int {
if v2 == 0 {
return 0
}
return int(math.Floor((v1 / v2) * 100))
}
func (s Sections) Len() int {
return len(s)
}
func (s Sections) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Sections) Less(i, j int) bool {
t1, t2 := s[i].Tally, s[j].Tally
return t1.Score() < t2.Score()
}
// GetObjectKind returns a schema object.
func (Section) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (s Section) DeepCopyObject() runtime.Object {
return s
}
// MaxSeverity gather the max severity in a collection of issues.
func (s Section) MaxSeverity() config.Level {
max := config.OkLevel
for _, issues := range s.Outcome {
m := issues.MaxSeverity()
if m > max {
max = m
}
}
return max
}
// MaxSeverity gather the max severity in a collection of issues.
func (i Issues) MaxSeverity() config.Level {
max := config.OkLevel
for _, is := range i {
if is.Level > max {
max = is.Level
}
}
return max
}
// CountSeverity counts severity level instances
func (i Issues) CountSeverity(l config.Level) int {
var count int
for _, is := range i {
if is.Level == l {
count++
}
}
return count
}

View File

@ -164,6 +164,9 @@ func (r RowEvents) FindIndex(id string) (int, bool) {
// Sort rows based on column index and order.
func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) {
if sortCol == -1 {
return
}
t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc}
sort.Sort(t)

View File

@ -26,7 +26,12 @@ type Command struct {
// NewCommand returns a new command view.
func NewCommand(styles *config.Styles, m *model.FishBuff) *Command {
c := Command{styles: styles, TextView: tview.NewTextView(), model: m, suggestionIndex: -1}
c := Command{
styles: styles,
TextView: tview.NewTextView(),
model: m,
suggestionIndex: -1,
}
c.SetWordWrap(true)
c.ShowCursor(true)
c.SetWrap(true)

View File

@ -203,7 +203,7 @@ func (t *Table) doUpdate(data render.TableData) {
}
custData := data.Customize(cols, t.wide)
if (t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1) && len(custData.Header) > 0 {
if (t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1) && len(custData.Header) > 0 && t.sortCol.name != "NONE" {
t.sortCol.name = custData.Header[0].Name
}

View File

@ -487,10 +487,6 @@ func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (a *App) viewResource(gvr, path string, clearStack bool) error {
return a.command.run(gvr, path, clearStack)
}
func (a *App) gotoResource(cmd, path string, clearStack bool) error {
return a.command.run(cmd, path, clearStack)
}

View File

@ -111,7 +111,6 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
if c.specialCmd(cmd) {
return nil
}
cmds := strings.Split(cmd, " ")
gvr, v, err := c.viewMetaFor(cmds[0])
if err != nil {

View File

@ -105,7 +105,7 @@ func podCtx(app *App, path, labelSel, fieldSel string) ContextFunc {
ns, _ := client.Namespaced(path)
mx := client.NewMetricsServer(app.factory.Client())
nmx, err := mx.FetchPodsMetrics(ns)
nmx, err := mx.FetchPodsMetrics(ctx, ns)
if err != nil {
log.Debug().Err(err).Msgf("No pods metrics")
}

View File

@ -2,6 +2,7 @@ package view
import (
"bytes"
"context"
"fmt"
"strings"
"time"
@ -132,9 +133,12 @@ func (n *Node) yamlCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
sel := n.GetTable().GetSelectedItem()
gvr := n.GVR().GVR()
o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(sel, metav1.GetOptions{})
o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(ctx, sel, metav1.GetOptions{})
if err != nil {
n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err)
return nil

112
internal/view/popeye.go Normal file
View File

@ -0,0 +1,112 @@
package view
import (
"context"
"fmt"
"strconv"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
)
// Popeye represents a sanitizer view.
type Popeye struct {
ResourceViewer
}
// NewPopeye returns a new view.
func NewPopeye(gvr client.GVR) ResourceViewer {
p := Popeye{
ResourceViewer: NewBrowser(gvr),
}
p.GetTable().SetColorerFn(render.Popeye{}.ColorerFunc())
p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)
p.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone)
p.GetTable().SetSortCol("SCORE%", true)
p.GetTable().SetDecorateFn(p.decorateRows)
p.SetBindKeysFn(p.bindKeys)
return &p
}
// Init initializes the view.
func (p *Popeye) Init(ctx context.Context) error {
if err := p.ResourceViewer.Init(ctx); err != nil {
return err
}
p.GetTable().GetModel().SetNamespace("*")
return nil
}
func (p *Popeye) decorateRows(data render.TableData) render.TableData {
var sum int
for _, re := range data.RowEvents {
n, err := strconv.Atoi(re.Row.Fields[1])
if err != nil {
continue
}
sum += n
}
score := sum / len(data.RowEvents)
p.GetTable().Path = fmt.Sprintf("Score %d -- %s", score, grade(score))
return data
}
func (p *Popeye) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Goto", p.describeCmd, true),
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false),
ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false),
ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false),
ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false),
ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false),
})
}
func (p *Popeye) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
path := p.GetTable().GetSelectedItem()
if path == "" {
return evt
}
v := NewSanitizer(client.NewGVR("report"))
v.SetContextFn(sanitizerCtx(path))
if err := p.App().inject(v); err != nil {
p.App().Flash().Err(err)
}
return nil
}
func sanitizerCtx(path string) ContextFunc {
return func(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, internal.KeyPath, path)
return ctx
}
}
// Helpers...
func grade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 70:
return "C"
case score >= 60:
return "D"
case score >= 50:
return "E"
default:
return "F"
}
}

View File

@ -72,6 +72,13 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("pulses")] = MetaViewer{
viewerFn: NewPulse,
}
vv[client.NewGVR("popeye")] = MetaViewer{
viewerFn: NewPopeye,
}
vv[client.NewGVR("report")] = MetaViewer{
viewerFn: NewSanitizer,
}
}
func appsViewers(vv MetaViewers) {

View File

@ -1,9 +1,11 @@
package view
import (
"context"
"errors"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
@ -43,8 +45,10 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
msg = fmt.Sprintf("Restart %d deployments?", len(paths))
}
dialog.ShowConfirm(r.App().Content.Pages, "Confirm Restart", msg, func() {
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
for _, path := range paths {
if err := r.restartRollout(path); err != nil {
if err := r.restartRollout(ctx, path); err != nil {
r.App().Flash().Err(err)
} else {
r.App().Flash().Infof("Rollout restart in progress for `%s...", path)
@ -55,7 +59,7 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (r *RestartExtender) restartRollout(path string) error {
func (r *RestartExtender) restartRollout(ctx context.Context, path string) error {
res, err := dao.AccessorFor(r.App().factory, r.GVR())
if err != nil {
return nil
@ -65,5 +69,5 @@ func (r *RestartExtender) restartRollout(path string) error {
return errors.New("resource is not restartable")
}
return s.Restart(path)
return s.Restart(ctx, path)
}

447
internal/view/sanitizer.go Normal file
View File

@ -0,0 +1,447 @@
package view
import (
"context"
"fmt"
"strings"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/xray"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ ResourceViewer = (*Sanitizer)(nil)
// Sanitizer represents an sanitizer tree view.
type Sanitizer struct {
*ui.Tree
app *App
gvr client.GVR
meta metav1.APIResource
model *model.Tree
cancelFn context.CancelFunc
envFn EnvFunc
contextFn ContextFunc
}
// NewSanitizer returns a new view.
func NewSanitizer(gvr client.GVR) ResourceViewer {
return &Sanitizer{
gvr: gvr,
Tree: ui.NewTree(),
model: model.NewTree(gvr),
}
}
// Init initializes the view
func (s *Sanitizer) Init(ctx context.Context) error {
s.envFn = s.k9sEnv
if err := s.Tree.Init(ctx); err != nil {
return err
}
s.SetKeyListenerFn(s.keyEntered)
var err error
s.meta, err = dao.MetaAccess.MetaFor(s.gvr)
if err != nil {
return err
}
if s.app, err = extractApp(ctx); err != nil {
return err
}
s.bindKeys()
s.SetBackgroundColor(s.app.Styles.Xray().BgColor.Color())
s.SetBorderColor(s.app.Styles.Xray().FgColor.Color())
s.SetBorderFocusColor(s.app.Styles.Frame().Border.FocusColor.Color())
s.SetGraphicsColor(s.app.Styles.Xray().GraphicColor.Color())
s.SetTitle(strings.Title(s.gvr.R()))
s.model.SetRefreshRate(time.Duration(s.app.Config.K9s.GetRefreshRate()) * time.Second)
s.model.SetNamespace(client.CleanseNamespace(s.app.Config.ActiveNamespace()))
s.model.AddListener(s)
s.SetChangedFunc(func(n *tview.TreeNode) {
spec, ok := n.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("No ref found on node %s", n.GetText())
return
}
s.SetSelectedItem(spec.AsPath())
s.refreshActions()
})
s.refreshActions()
return nil
}
// ExtraHints returns additional hints.
func (s *Sanitizer) ExtraHints() map[string]string {
if !s.app.Styles.Xray().ShowIcons {
return nil
}
return xray.EmojiInfo()
}
// SetInstance sets specific resource instance.
func (s *Sanitizer) SetInstance(string) {}
func (s *Sanitizer) bindKeys() {
s.Actions().Add(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Goto", s.gotoCmd, true),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", s.activateCmd, false),
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", s.eraseCmd, false),
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", s.eraseCmd, false),
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", s.eraseCmd, false),
tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", s.clearCmd, false),
tcell.KeyCtrlW: ui.NewSharedKeyAction("Clear Filter", s.clearCmd, false),
tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", s.resetCmd, false),
})
}
func (s *Sanitizer) keyEntered() {
s.ClearSelection()
s.update(s.filter(s.model.Peek()))
}
func (s *Sanitizer) refreshActions() {
}
// GetSelectedPath returns the current selection as string.
func (s *Sanitizer) GetSelectedPath() string {
spec := s.selectedSpec()
if spec == nil {
return ""
}
return spec.Path()
}
func (s *Sanitizer) selectedSpec() *xray.NodeSpec {
node := s.GetCurrentNode()
if node == nil {
return nil
}
ref, ok := node.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("Expecting a NodeSpec!")
return nil
}
return &ref
}
// EnvFn returns an plugin env function if available.
func (s *Sanitizer) EnvFn() EnvFunc {
return s.envFn
}
func (s *Sanitizer) k9sEnv() Env {
env := k8sEnv(s.app.Conn().Config())
spec := s.selectedSpec()
if spec == nil {
return env
}
env["FILTER"] = s.CmdBuff().String()
if env["FILTER"] == "" {
ns, n := client.Namespaced(spec.Path())
env["NAMESPACE"], env["FILTER"] = ns, n
}
switch spec.GVR() {
case "containers":
_, co := client.Namespaced(spec.Path())
env["CONTAINER"] = co
ns, n := client.Namespaced(*spec.ParentPath())
env["NAMESPACE"], env["POD"], env["NAME"] = ns, n, co
default:
ns, n := client.Namespaced(spec.Path())
env["NAMESPACE"], env["NAME"] = ns, n
}
return env
}
// Aliases returns all available aliases.
func (s *Sanitizer) Aliases() []string {
return append(s.meta.ShortNames, s.meta.SingularName, s.meta.Name)
}
func (s *Sanitizer) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if s.app.InCmdMode() {
return evt
}
s.CmdBuff().SetActive(true)
return nil
}
func (s *Sanitizer) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
if !s.CmdBuff().IsActive() {
return evt
}
s.CmdBuff().Clear()
s.model.ClearFilter()
s.Start()
return nil
}
func (s *Sanitizer) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if s.CmdBuff().IsActive() {
s.CmdBuff().Delete()
}
s.UpdateTitle()
return nil
}
func (s *Sanitizer) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !s.CmdBuff().InCmdMode() {
s.CmdBuff().Reset()
return s.app.PrevCmd(evt)
}
s.CmdBuff().Reset()
s.model.ClearFilter()
s.Start()
return nil
}
func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if s.CmdBuff().IsActive() {
if ui.IsLabelSelector(s.CmdBuff().String()) {
s.Start()
}
s.CmdBuff().SetActive(false)
s.GetRoot().ExpandAll()
return nil
}
spec := s.selectedSpec()
if spec == nil {
return nil
}
if len(spec.GVRs) <= 2 {
return nil
}
path := strings.Replace(spec.Path(), "::", "/", 1)
if strings.Contains(path, "[") {
return nil
}
if len(strings.Split(path, "/")) == 1 && spec.GVR() != "node" {
path = "-/" + path
}
if err := s.app.gotoResource(client.NewGVR(spec.GVR()).R(), path, false); err != nil {
log.Debug().Err(err)
}
return nil
}
func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode {
q := s.CmdBuff().String()
if s.CmdBuff().Empty() || ui.IsLabelSelector(q) {
return root
}
s.UpdateTitle()
if ui.IsFuzzySelector(q) {
return root.Filter(q, fuzzyFilter)
}
return root.Filter(q, rxFilter)
}
// TreeNodeSelected callback for node selection.
func (s *Sanitizer) TreeNodeSelected() {
s.app.QueueUpdateDraw(func() {
n := s.GetCurrentNode()
if n != nil {
n.SetColor(s.app.Styles.Xray().CursorColor.Color())
}
})
}
// TreeLoadFailed notifies the load failed.
func (s *Sanitizer) TreeLoadFailed(err error) {
s.app.Flash().Err(err)
}
func (s *Sanitizer) update(node *xray.TreeNode) {
root := makeTreeNode(node, s.ExpandNodes(), s.app.Styles)
if node == nil {
s.app.QueueUpdateDraw(func() {
s.SetRoot(root)
})
return
}
for _, c := range node.Children {
s.hydrate(root, c)
}
if s.GetSelectedItem() == "" {
s.SetSelectedItem(node.Spec().Path())
}
s.app.QueueUpdateDraw(func() {
s.SetRoot(root)
root.Walk(func(node, parent *tview.TreeNode) bool {
spec, ok := node.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("Expecting a NodeSpec but got %T", node.GetReference())
return false
}
// BOZO!! Figure this out expand/collapse but the root
if parent != nil {
node.SetExpanded(s.ExpandNodes())
} else {
node.SetExpanded(true)
}
if spec.AsPath() == s.GetSelectedItem() {
node.SetExpanded(true).SetSelectable(true)
s.SetCurrentNode(node)
}
return true
})
})
}
// TreeChanged notifies the model data changed.
func (s *Sanitizer) TreeChanged(node *xray.TreeNode) {
s.Count = node.Count(s.gvr.String())
s.update(s.filter(node))
s.UpdateTitle()
}
func (s *Sanitizer) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {
node := makeTreeNode(n, s.ExpandNodes(), s.app.Styles)
for _, c := range n.Children {
s.hydrate(node, c)
}
parent.AddChild(node)
}
// SetEnvFn sets the custom environment function.
func (s *Sanitizer) SetEnvFn(EnvFunc) {}
// Refresh updates the view
func (s *Sanitizer) Refresh() {
}
// BufferChanged indicates the buffer was changed.
func (s *Sanitizer) BufferChanged(t string) {}
// BufferActive indicates the buff activity changed.
func (s *Sanitizer) BufferActive(state bool, k model.BufferKind) {
s.app.BufferActive(state, k)
}
func (s *Sanitizer) defaultContext() context.Context {
ctx := context.WithValue(context.Background(), internal.KeyFactory, s.app.factory)
ctx = context.WithValue(ctx, internal.KeyFields, "")
if s.CmdBuff().Empty() {
ctx = context.WithValue(ctx, internal.KeyLabels, "")
} else {
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(s.CmdBuff().String()))
}
return ctx
}
// Start initializes resource watch loop.
func (s *Sanitizer) Start() {
s.Stop()
s.CmdBuff().AddListener(s.app.Cmd())
s.CmdBuff().AddListener(s)
ctx := s.defaultContext()
ctx, s.cancelFn = context.WithCancel(ctx)
if s.contextFn != nil {
ctx = s.contextFn(ctx)
}
s.model.Refresh(ctx)
s.UpdateTitle()
}
// Stop terminates watch loop.
func (s *Sanitizer) Stop() {
if s.cancelFn == nil {
return
}
s.cancelFn()
s.cancelFn = nil
s.CmdBuff().RemoveListener(s.app.Cmd())
s.CmdBuff().RemoveListener(s)
}
// SetBindKeysFn sets up extra key bindings.
func (s *Sanitizer) SetBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context.
func (s *Sanitizer) SetContextFn(f ContextFunc) {
s.contextFn = f
}
// Name returns the component name.
func (s *Sanitizer) Name() string { return "report" }
// GetTable returns the underlying table.
func (s *Sanitizer) GetTable() *Table { return nil }
// GVR returns a resource descriptor.
func (s *Sanitizer) GVR() client.GVR { return s.gvr }
// App returns the current app handle.
func (s *Sanitizer) App() *App {
return s.app
}
// UpdateTitle updates the view title.
func (s *Sanitizer) UpdateTitle() {
t := s.styleTitle()
s.app.QueueUpdateDraw(func() {
s.SetTitle(t)
})
}
func (s *Sanitizer) styleTitle() string {
base := strings.Title(s.gvr.R())
ns := s.model.GetNamespace()
if client.IsAllNamespaces(ns) {
ns = client.NamespaceAll
}
buff := s.CmdBuff().String()
var title string
if ns == client.ClusterScope {
title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, s.Count), s.app.Styles.Frame())
} else {
title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, s.Count), s.app.Styles.Frame())
}
if buff == "" {
return title
}
if ui.IsLabelSelector(buff) {
buff = ui.TrimLabelSelector(buff)
}
return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), s.app.Styles.Frame())
}

View File

@ -1,10 +1,12 @@
package view
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
@ -73,7 +75,9 @@ func (s *ScaleExtender) makeScaleForm(sel string) *tview.Form {
s.App().Flash().Err(err)
return
}
if err := s.scale(sel, count); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
if err := s.scale(ctx, sel, count); err != nil {
log.Error().Err(err).Msgf("DP %s scaling failed", sel)
s.App().Flash().Err(err)
} else {
@ -104,7 +108,7 @@ func (s *ScaleExtender) makeStyledForm() *tview.Form {
return f
}
func (s *ScaleExtender) scale(path string, replicas int) error {
func (s *ScaleExtender) scale(ctx context.Context, path string, replicas int) error {
res, err := dao.AccessorFor(s.App().factory, s.GVR())
if err != nil {
return nil
@ -114,5 +118,5 @@ func (s *ScaleExtender) scale(path string, replicas int) error {
return fmt.Errorf("expecting a scalable resource for %q", s.GVR())
}
return scaler.Scale(path, int32(replicas))
return scaler.Scale(ctx, path, int32(replicas))
}

View File

@ -470,11 +470,10 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if spec == nil {
return nil
}
log.Debug().Msgf("SELECTED REF %#v", spec)
if len(strings.Split(spec.Path(), "/")) == 1 {
return nil
}
if err := x.app.viewResource(client.NewGVR(spec.GVR()).R(), spec.Path(), false); err != nil {
if err := x.app.gotoResource(client.NewGVR(spec.GVR()).R(), spec.Path(), false); err != nil {
x.app.Flash().Err(err)
}
@ -542,7 +541,6 @@ func (x *Xray) update(node *xray.TreeNode) {
}
if spec.AsPath() == x.GetSelectedItem() {
log.Debug().Msgf("SEL %q--%q", spec.Path(), x.GetSelectedItem())
node.SetExpanded(true).SetSelectable(true)
x.SetCurrentNode(node)
}

View File

@ -35,9 +35,7 @@ func (c *Container) Render(ctx context.Context, ns string, o interface{}) error
}
pns, _ := client.Namespaced(parent.ID)
c.envRefs(f, root, pns, co.Container)
// if !root.IsLeaf() {
parent.Add(root)
// }
return nil
}

83
internal/xray/section.go Normal file
View File

@ -0,0 +1,83 @@
package xray
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/popeye/pkg/config"
)
// Section represents an xray renderer.
type Section struct{}
// Render renders an xray node.
func (s *Section) Render(ctx context.Context, ns string, o interface{}) error {
section, ok := o.(render.Section)
if !ok {
return fmt.Errorf("Expected Section, but got %T", o)
}
root := NewTreeNode(section.Title, section.Title)
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
s.outcomeRefs(root, section)
parent.Add(root)
return nil
}
func cleanse(s string) string {
s = strings.Replace(s, "[", "(", -1)
s = strings.Replace(s, "]", ")", -1)
s = strings.Replace(s, "/", "::", -1)
return s
}
func (c *Section) outcomeRefs(parent *TreeNode, section render.Section) {
for k, issues := range section.Outcome {
p := NewTreeNode(section.Title, cleanse(k))
parent.Add(p)
for _, i := range issues {
msg := colorize(cleanse(i.Message), i.Level)
c := NewTreeNode(fmt.Sprintf("issue_%d", i.Level), msg)
if i.Group == "__root__" {
p.Add(c)
continue
}
if pa := p.Find(childOf(section.Title), i.Group); pa != nil {
pa.Add(c)
continue
}
pa := NewTreeNode(childOf(section.Title), i.Group)
pa.Add(c)
p.Add(pa)
}
}
}
func childOf(s string) string {
switch s {
case "deployment", "statefulset", "daemonset":
return "v1/pods"
case "pod":
return "containers"
default:
return ""
}
}
func colorize(s string, l config.Level) string {
c := "green"
switch l {
case config.ErrorLevel:
c = "red"
case config.WarnLevel:
c = "orange"
case config.InfoLevel:
c = "blue"
}
return fmt.Sprintf("[%s::]%s", c, s)
}

View File

@ -469,36 +469,70 @@ func (t TreeNode) toEmojiTitle() (title string) {
}
func toEmoji(gvr string) string {
if ic := toEmojiXRay(gvr); ic != "" {
return ic
}
switch gvr {
case "containers":
return "🐳"
case "v1/namespaces", "namespaces":
return "🗂"
case "v1/pods", "pods":
return "🚛"
case "v1/services", "services":
return "💁‍♀️"
case "v1/serviceaccounts", "serviceaccounts":
return "💳"
case "v1/persistentvolumes", "persistentvolumes":
return "📚"
case "v1/persistentvolumeclaims", "persistentvolumeclaims":
return "🎟"
case "v1/secrets", "secrets":
return "🔒"
case "v1/configmaps", "configmaps":
return "🗺"
case "apps/v1/deployments", "deployments":
return "🪂"
case "apps/v1/statefulsets", "statefulsets":
return "🎎"
case "apps/v1/daemonsets", "daemonsets":
return "😈"
case "replicasets", "replicaset":
return "👯‍♂️"
case "nodes", "node":
return "🖥 "
case "horizontalpodautoscalers", "horizontalpodautoscaler":
return "♎️"
case "clusterrolebindings", "clusterrolebinding", "clusterroles", "clusterrole":
return "👩‍"
case "rolebindings", "rolebinding", "roles", "role":
return "👨🏻‍"
case "networkpolicies", "networkpolicy":
return "📕"
case "poddisruptionbudgets", "poddisruptionbudget":
return "🏷 "
case "issue_0":
return "👍"
case "issue_1":
return "🔊"
case "issue_2":
return "☣️ "
case "issue_3":
return "🧨"
case "report":
return "🧼"
default:
return "📎"
}
}
func toEmojiXRay(gvr string) string {
switch gvr {
case "containers", "container":
return "🐳"
case "v1/namespaces", "namespaces", "namespace":
return "🗂 "
case "v1/pods", "pods", "pod":
return "🚛"
case "v1/services", "services", "service":
return "💁‍♀️"
case "v1/serviceaccounts", "serviceaccounts", "serviceaccount":
return "💳"
case "v1/persistentvolumes", "persistentvolumes", "persistentvolume":
return "📚"
case "v1/persistentvolumeclaims", "persistentvolumeclaims", "persistentvolumeclaim":
return "🎟 "
case "v1/secrets", "secrets", "secret":
return "🔒"
case "v1/configmaps", "configmaps", "configmap":
return "🗺 "
case "apps/v1/deployments", "deployments", "deployment":
return "🪂"
case "apps/v1/statefulsets", "statefulsets", "statefulset":
return "🎎"
case "apps/v1/daemonsets", "daemonsets", "daemonset":
return "😈"
default:
return ""
}
}
// EmojiInfo returns emoji help.
func EmojiInfo() map[string]string {
GVRs := []string{