parent
a8771b8c67
commit
c917b6c548
|
|
@ -10,6 +10,7 @@ for changes and offers subsequent commands to interact with observed Kubernetes
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/derailed/k9s)
|
[](https://goreportcard.com/report/github.com/derailed/k9s)
|
||||||
|
[](https://golangci.com/r/github.com/derailed/k9s)
|
||||||
[](https://codebeat.co/projects/github-com-derailed-k9s-master)
|
[](https://codebeat.co/projects/github-com-derailed-k9s-master)
|
||||||
[](https://travis-ci.com/derailed/k9s)
|
[](https://travis-ci.com/derailed/k9s)
|
||||||
[](https://quay.io/repository/derailed/k9s)
|
[](https://quay.io/repository/derailed/k9s)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||||
|
|
||||||
|
# Release v0.11.3
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
|
||||||
|
|
||||||
|
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_helm.png" align="center" width="300" height="auto"/>
|
||||||
|
|
||||||
|
Maintenance Release!
|
||||||
|
|
||||||
|
### Speedy Gonzales?
|
||||||
|
|
||||||
|
In this drop, we took a bit of a perf pass in light of recent issues and thanks to [Chris Werner Rau](https://github.com/cwrau) pushing me and keeping me up to speed, I've digged a bit deeper and found that there might be some seamingly innocent calls that sucked a bit of cycles during K9s refreshes. Long story short, I think this drop will improve perf by a factor of ~10x in some instances. Typically the initial load will be slower but subsequent loads should be much faster. Famous last words right? Anyhow, can't really take credit for this one as the awesome [Gustavo Silva Paiva](https://github.com/paivagustavo) suggested doing this a while back, but since I was already in flight with the refactor decided to punt until back online. And there we are...
|
||||||
|
|
||||||
|
Hopefully these findings will coalesce with yours?? If not, please send bulk prozac patches at the address below...
|
||||||
|
|
||||||
|
Thanks Chris! Was up all night trying to figure out what was the deal with K9s and your specific clusters. Hopefully this time for sure??
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolved Bugs/Features
|
||||||
|
|
||||||
|
* [Issue #475](https://github.com/derailed/k9s/issues/475)
|
||||||
|
* [Issue #473](https://github.com/derailed/k9s/issues/473)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<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)
|
||||||
|
|
@ -3,6 +3,7 @@ package client
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -10,6 +11,7 @@ import (
|
||||||
authorizationv1 "k8s.io/api/authorization/v1"
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
"k8s.io/apimachinery/pkg/version"
|
"k8s.io/apimachinery/pkg/version"
|
||||||
"k8s.io/client-go/discovery/cached/disk"
|
"k8s.io/client-go/discovery/cached/disk"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
|
|
@ -19,6 +21,12 @@ import (
|
||||||
versioned "k8s.io/metrics/pkg/client/clientset/versioned"
|
versioned "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cacheSize = 100
|
||||||
|
cacheExpiry = 5 * time.Minute
|
||||||
|
cacheMXKey = "metrics"
|
||||||
|
)
|
||||||
|
|
||||||
var supportedMetricsAPIVersions = []string{"v1beta1"}
|
var supportedMetricsAPIVersions = []string{"v1beta1"}
|
||||||
|
|
||||||
// Authorizer checks what a user can or cannot do to a resource.
|
// Authorizer checks what a user can or cannot do to a resource.
|
||||||
|
|
@ -29,23 +37,26 @@ type Authorizer interface {
|
||||||
|
|
||||||
// APIClient represents a Kubernetes api client.
|
// APIClient represents a Kubernetes api client.
|
||||||
type APIClient struct {
|
type APIClient struct {
|
||||||
client kubernetes.Interface
|
client kubernetes.Interface
|
||||||
dClient dynamic.Interface
|
dClient dynamic.Interface
|
||||||
nsClient dynamic.NamespaceableResourceInterface
|
nsClient dynamic.NamespaceableResourceInterface
|
||||||
mxsClient *versioned.Clientset
|
mxsClient *versioned.Clientset
|
||||||
cachedDiscovery *disk.CachedDiscoveryClient
|
cachedClient *disk.CachedDiscoveryClient
|
||||||
config *Config
|
config *Config
|
||||||
useMetricServer bool
|
mx sync.Mutex
|
||||||
mx sync.Mutex
|
cache *cache.LRUExpireCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitConnectionOrDie initialize connection from command line args.
|
// InitConnectionOrDie initialize connection from command line args.
|
||||||
// Checks for connectivity with the api server.
|
// Checks for connectivity with the api server.
|
||||||
func InitConnectionOrDie(config *Config) *APIClient {
|
func InitConnectionOrDie(config *Config) *APIClient {
|
||||||
conn := APIClient{config: config}
|
a := APIClient{
|
||||||
conn.useMetricServer = conn.supportsMxServer()
|
config: config,
|
||||||
|
cache: cache.NewLRUExpireCache(cacheSize),
|
||||||
|
}
|
||||||
|
a.HasMetrics()
|
||||||
|
|
||||||
return &conn
|
return &a
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
||||||
|
|
@ -66,24 +77,44 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeKey(ns, gvr string, vv []string) string {
|
||||||
|
return ns + ":" + gvr + "::" + strings.Join(vv, ",")
|
||||||
|
}
|
||||||
|
|
||||||
// CanI checks if user has access to a certain resource.
|
// CanI checks if user has access to a certain resource.
|
||||||
func (a *APIClient) CanI(ns, gvr string, verbs []string) (bool, error) {
|
func (a *APIClient) CanI(ns, gvr string, verbs []string) (bool, error) {
|
||||||
|
defer func(t time.Time) {
|
||||||
|
log.Debug().Msgf("AUTH elapsed %v", time.Since(t))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
log.Debug().Msgf("AUTH %q:%q -- %v", ns, gvr, verbs)
|
log.Debug().Msgf("AUTH %q:%q -- %v", ns, gvr, verbs)
|
||||||
sar := makeSAR(ns, gvr)
|
sar := makeSAR(ns, gvr)
|
||||||
|
|
||||||
|
key := makeKey(ns, gvr, verbs)
|
||||||
|
if v, ok := a.cache.Get(key); ok {
|
||||||
|
if auth, ok := v.(bool); ok {
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dial := a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews()
|
dial := a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews()
|
||||||
for _, v := range verbs {
|
for _, v := range verbs {
|
||||||
sar.Spec.ResourceAttributes.Verb = v
|
sar.Spec.ResourceAttributes.Verb = v
|
||||||
resp, err := dial.Create(sar)
|
resp, err := dial.Create(sar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msgf(" Dial Failed!")
|
log.Warn().Err(err).Msgf(" Dial Failed!")
|
||||||
|
a.cache.Add(key, false, cacheExpiry)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if !resp.Status.Allowed {
|
if !resp.Status.Allowed {
|
||||||
log.Debug().Msgf(" NO %q ;(", v)
|
log.Debug().Msgf(" NO %q ;(", v)
|
||||||
|
a.cache.Add(key, false, cacheExpiry)
|
||||||
return false, fmt.Errorf("`%s access denied for user on %q:%s", v, ns, gvr)
|
return false, fmt.Errorf("`%s access denied for user on %q:%s", v, ns, gvr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msgf(" YES!")
|
log.Debug().Msgf(" YES!")
|
||||||
|
a.cache.Add(key, true, cacheExpiry)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,12 +125,7 @@ func (a *APIClient) CurrentNamespaceName() (string, error) {
|
||||||
|
|
||||||
// ServerVersion returns the current server version info.
|
// ServerVersion returns the current server version info.
|
||||||
func (a *APIClient) ServerVersion() (*version.Info, error) {
|
func (a *APIClient) ServerVersion() (*version.Info, error) {
|
||||||
discovery, err := a.CachedDiscovery()
|
return a.CachedDiscoveryOrDie().ServerVersion()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return discovery.ServerVersion()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidNamespaces returns all available namespaces.
|
// ValidNamespaces returns all available namespaces.
|
||||||
|
|
@ -113,12 +139,7 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
|
||||||
|
|
||||||
// IsNamespaced check on server if given resource is namespaced
|
// IsNamespaced check on server if given resource is namespaced
|
||||||
func (a *APIClient) IsNamespaced(res string) bool {
|
func (a *APIClient) IsNamespaced(res string) bool {
|
||||||
discovery, err := a.CachedDiscovery()
|
list, _ := a.CachedDiscoveryOrDie().ServerPreferredResources()
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
list, _ := discovery.ServerPreferredResources()
|
|
||||||
for _, l := range list {
|
for _, l := range list {
|
||||||
for _, r := range l.APIResources {
|
for _, r := range l.APIResources {
|
||||||
if r.Name == res {
|
if r.Name == res {
|
||||||
|
|
@ -131,12 +152,7 @@ func (a *APIClient) IsNamespaced(res string) bool {
|
||||||
|
|
||||||
// SupportsResource checks for resource supported version against the server.
|
// SupportsResource checks for resource supported version against the server.
|
||||||
func (a *APIClient) SupportsResource(group string) bool {
|
func (a *APIClient) SupportsResource(group string) bool {
|
||||||
discovery, err := a.CachedDiscovery()
|
list, err := a.CachedDiscoveryOrDie().ServerPreferredResources()
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
list, err := discovery.ServerPreferredResources()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Unable to dial api server")
|
log.Error().Err(err).Msg("Unable to dial api server")
|
||||||
return false
|
return false
|
||||||
|
|
@ -157,7 +173,13 @@ func (a *APIClient) Config() *Config {
|
||||||
|
|
||||||
// HasMetrics returns true if the cluster supports metrics.
|
// HasMetrics returns true if the cluster supports metrics.
|
||||||
func (a *APIClient) HasMetrics() bool {
|
func (a *APIClient) HasMetrics() bool {
|
||||||
return a.useMetricServer
|
v, ok := a.cache.Get(cacheMXKey)
|
||||||
|
if !ok {
|
||||||
|
return a.supportsMxServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
flag, ok := v.(bool)
|
||||||
|
return ok && flag
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialOrDie returns a handle to api server or die.
|
// DialOrDie returns a handle to api server or die.
|
||||||
|
|
@ -183,12 +205,12 @@ func (a *APIClient) RestConfigOrDie() *restclient.Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CachedDiscovery returns a cached discovery client.
|
// CachedDiscovery returns a cached discovery client.
|
||||||
func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
|
func (a *APIClient) CachedDiscoveryOrDie() *disk.CachedDiscoveryClient {
|
||||||
a.mx.Lock()
|
a.mx.Lock()
|
||||||
defer a.mx.Unlock()
|
defer a.mx.Unlock()
|
||||||
|
|
||||||
if a.cachedDiscovery != nil {
|
if a.cachedClient != nil {
|
||||||
return a.cachedDiscovery, nil
|
return a.cachedClient
|
||||||
}
|
}
|
||||||
|
|
||||||
rc := a.RestConfigOrDie()
|
rc := a.RestConfigOrDie()
|
||||||
|
|
@ -196,8 +218,11 @@ func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
|
||||||
discCacheDir := filepath.Join(mustHomeDir(), ".kube", "cache", "discovery", toHostDir(rc.Host))
|
discCacheDir := filepath.Join(mustHomeDir(), ".kube", "cache", "discovery", toHostDir(rc.Host))
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
a.cachedDiscovery, err = disk.NewCachedDiscoveryClientForConfig(rc, discCacheDir, httpCacheDir, 10*time.Minute)
|
a.cachedClient, err = disk.NewCachedDiscoveryClientForConfig(rc, discCacheDir, httpCacheDir, 10*time.Minute)
|
||||||
return a.cachedDiscovery, err
|
if err != nil {
|
||||||
|
log.Panic().Msgf("Unable to connect to discovery client %v", err)
|
||||||
|
}
|
||||||
|
return a.cachedClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// DynDialOrDie returns a handle to a dynamic interface.
|
// DynDialOrDie returns a handle to a dynamic interface.
|
||||||
|
|
@ -237,12 +262,12 @@ func (a *APIClient) SwitchContextOrDie(ctx string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentCtx != ctx {
|
if currentCtx != ctx {
|
||||||
a.cachedDiscovery = nil
|
a.cachedClient = nil
|
||||||
a.reset()
|
a.reset()
|
||||||
if err := a.config.SwitchContext(ctx); err != nil {
|
if err := a.config.SwitchContext(ctx); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Switching context")
|
log.Fatal().Err(err).Msg("Switching context")
|
||||||
}
|
}
|
||||||
a.useMetricServer = a.supportsMxServer()
|
_ = a.supportsMxServer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,27 +278,26 @@ func (a *APIClient) reset() {
|
||||||
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
|
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *APIClient) supportsMxServer() bool {
|
func (a *APIClient) supportsMxServer() (supported bool) {
|
||||||
discovery, err := a.CachedDiscovery()
|
defer func() {
|
||||||
if err != nil {
|
a.cache.Add(cacheMXKey, supported, cacheExpiry)
|
||||||
return false
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
apiGroups, err := discovery.ServerGroups()
|
apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, grp := range apiGroups.Groups {
|
for _, grp := range apiGroups.Groups {
|
||||||
if grp.Name != metricsapi.GroupName {
|
if grp.Name != metricsapi.GroupName {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if checkMetricsVersion(grp) {
|
if checkMetricsVersion(grp) {
|
||||||
return true
|
supported = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMetricsVersion(grp metav1.APIGroup) bool {
|
func checkMetricsVersion(grp metav1.APIGroup) bool {
|
||||||
|
|
@ -290,16 +314,10 @@ func checkMetricsVersion(grp metav1.APIGroup) bool {
|
||||||
|
|
||||||
// SupportsRes checks latest supported version.
|
// SupportsRes checks latest supported version.
|
||||||
func (a *APIClient) SupportsRes(group string, versions []string) (string, bool, error) {
|
func (a *APIClient) SupportsRes(group string, versions []string) (string, bool, error) {
|
||||||
discovery, err := a.CachedDiscovery()
|
apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiGroups, err := discovery.ServerGroups()
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, grp := range apiGroups.Groups {
|
for _, grp := range apiGroups.Groups {
|
||||||
if grp.Name != group {
|
if grp.Name != group {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@ func (g GVRs) Less(i, j int) bool {
|
||||||
|
|
||||||
// Can determines the available actions for a given resource.
|
// Can determines the available actions for a given resource.
|
||||||
func Can(verbs []string, v string) bool {
|
func Can(verbs []string, v string) bool {
|
||||||
|
if len(verbs) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
for _, verb := range verbs {
|
for _, verb := range verbs {
|
||||||
candidates, err := mapVerb(v)
|
candidates, err := mapVerb(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -70,33 +71,41 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
||||||
|
|
||||||
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
||||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||||
|
var mx mv1beta1.NodeMetricsList
|
||||||
|
if !m.HasMetrics() {
|
||||||
|
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||||
|
}
|
||||||
|
|
||||||
auth, err := m.CanI("", "metrics.k8s.io/v1beta1/nodes", []string{"list"})
|
auth, err := m.CanI("", "metrics.k8s.io/v1beta1/nodes", []string{"list"})
|
||||||
if !auth || err != nil {
|
if !auth || err != nil {
|
||||||
return nil, err
|
return &mx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := m.MXDial()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return &mx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
return client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
||||||
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
||||||
|
var mx mv1beta1.PodMetricsList
|
||||||
|
if !m.HasMetrics() {
|
||||||
|
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||||
|
}
|
||||||
if ns == NamespaceAll {
|
if ns == NamespaceAll {
|
||||||
ns = AllNamespaces
|
ns = AllNamespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", []string{"list"})
|
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", []string{"list"})
|
||||||
if !auth || err != nil {
|
if !auth || err != nil {
|
||||||
return &mv1beta1.PodMetricsList{}, err
|
return &mx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := m.MXDial()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return &mx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
|
return client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
|
||||||
|
|
@ -104,17 +113,22 @@ func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, e
|
||||||
|
|
||||||
// FetchPodMetrics return all metrics for pods in a given namespace.
|
// FetchPodMetrics return all metrics for pods in a given namespace.
|
||||||
func (m *MetricsServer) FetchPodMetrics(ns, sel string) (*mv1beta1.PodMetrics, error) {
|
func (m *MetricsServer) FetchPodMetrics(ns, sel string) (*mv1beta1.PodMetrics, error) {
|
||||||
|
var mx mv1beta1.PodMetrics
|
||||||
|
if !m.HasMetrics() {
|
||||||
|
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||||
|
}
|
||||||
|
|
||||||
if ns == NamespaceAll {
|
if ns == NamespaceAll {
|
||||||
ns = AllNamespaces
|
ns = AllNamespaces
|
||||||
}
|
}
|
||||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", []string{"get"})
|
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", []string{"get"})
|
||||||
if !auth || err != nil {
|
if !auth || err != nil {
|
||||||
return nil, err
|
return &mx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := m.MXDial()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return &mx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.MetricsV1beta1().PodMetricses(ns).Get(sel, metav1.GetOptions{})
|
return client.MetricsV1beta1().PodMetricses(ns).Get(sel, metav1.GetOptions{})
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ type Connection interface {
|
||||||
Config() *Config
|
Config() *Config
|
||||||
DialOrDie() kubernetes.Interface
|
DialOrDie() kubernetes.Interface
|
||||||
SwitchContextOrDie(ctx string)
|
SwitchContextOrDie(ctx string)
|
||||||
CachedDiscovery() (*disk.CachedDiscoveryClient, error)
|
CachedDiscoveryOrDie() *disk.CachedDiscoveryClient
|
||||||
RestConfigOrDie() *restclient.Config
|
RestConfigOrDie() *restclient.Config
|
||||||
MXDial() (*versioned.Clientset, error)
|
MXDial() (*versioned.Clientset, error)
|
||||||
DynDialOrDie() dynamic.Interface
|
DynDialOrDie() dynamic.Interface
|
||||||
|
|
|
||||||
|
|
@ -33,23 +33,19 @@ func NewMockConnection(options ...pegomock.Option) *MockConnection {
|
||||||
func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||||
func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail }
|
func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||||
|
|
||||||
func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
|
func (mock *MockConnection) CachedDiscoveryOrDie() *disk.CachedDiscoveryClient {
|
||||||
if mock == nil {
|
if mock == nil {
|
||||||
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
||||||
}
|
}
|
||||||
params := []pegomock.Param{}
|
params := []pegomock.Param{}
|
||||||
result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscoveryOrDie", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
var ret0 *disk.CachedDiscoveryClient
|
var ret0 *disk.CachedDiscoveryClient
|
||||||
var ret1 error
|
|
||||||
if len(result) != 0 {
|
if len(result) != 0 {
|
||||||
if result[0] != nil {
|
if result[0] != nil {
|
||||||
ret0 = result[0].(*disk.CachedDiscoveryClient)
|
ret0 = result[0].(*disk.CachedDiscoveryClient)
|
||||||
}
|
}
|
||||||
if result[1] != nil {
|
|
||||||
ret1 = result[1].(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ret0, ret1
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) {
|
func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) {
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,5 @@ func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtim
|
||||||
}
|
}
|
||||||
|
|
||||||
const gvr = "apiextensions.k8s.io/v1beta1/customresourcedefinitions"
|
const gvr = "apiextensions.k8s.io/v1beta1/customresourcedefinitions"
|
||||||
oo, err := c.Factory.List(gvr, "-", true, lsel)
|
return c.Factory.List(gvr, "-", true, lsel)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return oo, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,9 @@ type Generic struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a collection of resources.
|
// List returns a collection of resources.
|
||||||
|
// BOZO!! no auth check??
|
||||||
func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
log.Debug().Msgf("GENERIC LIST %q:%q", ns, g.gvr)
|
log.Debug().Msgf("GENERIC-LIST %q:%q", ns, g.gvr)
|
||||||
labelSel, ok := ctx.Value(internal.KeyLabels).(string)
|
labelSel, ok := ctx.Value(internal.KeyLabels).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warn().Msgf("No label selector found in context. Listing all resources")
|
log.Warn().Msgf("No label selector found in context. Listing all resources")
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ type Node struct {
|
||||||
|
|
||||||
// List returns a collection of node resources.
|
// List returns a collection of node resources.
|
||||||
func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
|
log.Debug().Msgf("NODE-LIST %q:%q", ns, n.gvr)
|
||||||
|
|
||||||
nmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.NodeMetricsList)
|
nmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.NodeMetricsList)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warn().Msgf("No node metrics available in context")
|
log.Warn().Msgf("No node metrics available in context")
|
||||||
|
|
|
||||||
|
|
@ -187,11 +187,7 @@ func loadRBAC(m ResourceMetas) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPreferred(f Factory, m ResourceMetas) error {
|
func loadPreferred(f Factory, m ResourceMetas) error {
|
||||||
discovery, err := f.Client().CachedDiscovery()
|
rr, err := f.Client().CachedDiscoveryOrDie().ServerPreferredResources()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rr, err := discovery.ServerPreferredResources()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msgf("Failed to load preferred resources")
|
log.Warn().Err(err).Msgf("Failed to load preferred resources")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
@ -20,6 +21,23 @@ type Resource struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns a collection of resources.
|
||||||
|
func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
|
log.Debug().Msgf("INF-LIST %q:%q", ns, r.gvr)
|
||||||
|
strLabel, ok := ctx.Value(internal.KeyLabels).(string)
|
||||||
|
lsel := labels.Everything()
|
||||||
|
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {
|
||||||
|
lsel = sel.AsSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Factory.List(r.gvr.String(), ns, false, lsel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a resource instance if found, else an error.
|
||||||
|
func (r *Resource) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||||
|
return r.Factory.Get(r.gvr.String(), path, true, labels.Everything())
|
||||||
|
}
|
||||||
|
|
||||||
// ToYAML returns a resource yaml.
|
// ToYAML returns a resource yaml.
|
||||||
func (r *Resource) ToYAML(path string) (string, error) {
|
func (r *Resource) ToYAML(path string) (string, error) {
|
||||||
o, err := r.Get(context.Background(), path)
|
o, err := r.Get(context.Background(), path)
|
||||||
|
|
@ -33,19 +51,3 @@ func (r *Resource) ToYAML(path string) (string, error) {
|
||||||
}
|
}
|
||||||
return raw, nil
|
return raw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns a resource instance if found, else an error.
|
|
||||||
func (r *Resource) Get(ctx context.Context, path string) (runtime.Object, error) {
|
|
||||||
return r.Factory.Get(r.gvr.String(), path, true, labels.Everything())
|
|
||||||
}
|
|
||||||
|
|
||||||
// List returns a collection of resources.
|
|
||||||
func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|
||||||
strLabel, ok := ctx.Value(internal.KeyLabels).(string)
|
|
||||||
lsel := labels.Everything()
|
|
||||||
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {
|
|
||||||
lsel = sel.AsSelector()
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Factory.List(r.gvr.String(), ns, false, lsel)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,10 @@ type RestMapper struct {
|
||||||
|
|
||||||
// ToRESTMapper map resources to kind, and map kind and version to interfaces for manipulating K8s objects.
|
// ToRESTMapper map resources to kind, and map kind and version to interfaces for manipulating K8s objects.
|
||||||
func (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) {
|
func (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) {
|
||||||
disc, err := r.CachedDiscovery()
|
dial := r.CachedDiscoveryOrDie()
|
||||||
if err != nil {
|
mapper := restmapper.NewDeferredDiscoveryRESTMapper(dial)
|
||||||
return nil, err
|
expander := restmapper.NewShortcutExpander(mapper, dial)
|
||||||
}
|
|
||||||
mapper := restmapper.NewDeferredDiscoveryRESTMapper(disc)
|
|
||||||
expander := restmapper.NewShortcutExpander(mapper, disc)
|
|
||||||
return expander, nil
|
return expander, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -18,6 +19,7 @@ type Table struct {
|
||||||
|
|
||||||
// List all Resources in a given namespace.
|
// List all Resources in a given namespace.
|
||||||
func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
|
log.Debug().Msgf("TABLE-LIST %q:%q", ns, t.gvr)
|
||||||
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
|
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
|
||||||
_, codec := t.codec()
|
_, codec := t.codec()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ var Registry = map[string]ResourceMeta{
|
||||||
Renderer: &render.Node{},
|
Renderer: &render.Node{},
|
||||||
},
|
},
|
||||||
"v1/services": {
|
"v1/services": {
|
||||||
|
DAO: &dao.Service{},
|
||||||
Renderer: &render.Service{},
|
Renderer: &render.Service{},
|
||||||
},
|
},
|
||||||
"v1/serviceaccounts": {
|
"v1/serviceaccounts": {
|
||||||
|
|
@ -87,15 +88,18 @@ var Registry = map[string]ResourceMeta{
|
||||||
|
|
||||||
// Apps...
|
// Apps...
|
||||||
"apps/v1/deployments": {
|
"apps/v1/deployments": {
|
||||||
|
DAO: &dao.Deployment{},
|
||||||
Renderer: &render.Deployment{},
|
Renderer: &render.Deployment{},
|
||||||
},
|
},
|
||||||
"apps/v1/replicasets": {
|
"apps/v1/replicasets": {
|
||||||
Renderer: &render.ReplicaSet{},
|
Renderer: &render.ReplicaSet{},
|
||||||
},
|
},
|
||||||
"apps/v1/statefulsets": {
|
"apps/v1/statefulsets": {
|
||||||
|
DAO: &dao.StatefulSet{},
|
||||||
Renderer: &render.StatefulSet{},
|
Renderer: &render.StatefulSet{},
|
||||||
},
|
},
|
||||||
"apps/v1/daemonsets": {
|
"apps/v1/daemonsets": {
|
||||||
|
DAO: &dao.DaemonSet{},
|
||||||
Renderer: &render.DaemonSet{},
|
Renderer: &render.DaemonSet{},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -115,6 +119,7 @@ var Registry = map[string]ResourceMeta{
|
||||||
|
|
||||||
// Batch...
|
// Batch...
|
||||||
"batch/v1beta1/cronjobs": {
|
"batch/v1beta1/cronjobs": {
|
||||||
|
DAO: &dao.CronJob{},
|
||||||
Renderer: &render.CronJob{},
|
Renderer: &render.CronJob{},
|
||||||
},
|
},
|
||||||
"batch/v1/jobs": {
|
"batch/v1/jobs": {
|
||||||
|
|
@ -138,11 +143,13 @@ var Registry = map[string]ResourceMeta{
|
||||||
|
|
||||||
// CRDs...
|
// CRDs...
|
||||||
"apiextensions.k8s.io/v1/customresourcedefinitions": {
|
"apiextensions.k8s.io/v1/customresourcedefinitions": {
|
||||||
DAO: &dao.CustomResourceDefinition{},
|
// BOZO!!
|
||||||
|
// DAO: &dao.CustomResourceDefinition{},
|
||||||
Renderer: &render.CustomResourceDefinition{},
|
Renderer: &render.CustomResourceDefinition{},
|
||||||
},
|
},
|
||||||
"apiextensions.k8s.io/v1beta1/customresourcedefinitions": {
|
"apiextensions.k8s.io/v1beta1/customresourcedefinitions": {
|
||||||
DAO: &dao.CustomResourceDefinition{},
|
// BOZO!!
|
||||||
|
// DAO: &dao.CustomResourceDefinition{},
|
||||||
Renderer: &render.CustomResourceDefinition{},
|
Renderer: &render.CustomResourceDefinition{},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,8 +193,8 @@ func (t *Table) refresh(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {
|
func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {
|
||||||
defer func(t time.Time) {
|
defer func(ti time.Time) {
|
||||||
log.Debug().Msgf(" LIST elapsed %v", time.Since(t))
|
log.Debug().Msgf(" LIST %q:%q elapsed %v", t.namespace, t.gvr, time.Since(ti))
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
|
||||||
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
|
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
|
||||||
|
|
@ -207,8 +207,8 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) reconcile(ctx context.Context) error {
|
func (t *Table) reconcile(ctx context.Context) error {
|
||||||
defer func(t time.Time) {
|
defer func(ti time.Time) {
|
||||||
log.Debug().Msgf("RECONCILE elapsed %v", time.Since(t))
|
log.Debug().Msgf("RECONCILE %q:%q elapsed %v", t.namespace, t.gvr, time.Since(ti))
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
|
||||||
meta := t.resourceMeta()
|
meta := t.resourceMeta()
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error
|
||||||
func (c *Configurator) InitBench(cluster string) {
|
func (c *Configurator) InitBench(cluster string) {
|
||||||
var err error
|
var err error
|
||||||
if c.Bench, err = config.NewBench(BenchConfig(cluster)); err != nil {
|
if c.Bench, err = config.NewBench(BenchConfig(cluster)); err != nil {
|
||||||
log.Info().Err(err).Msg("No benchmark config file found, using defaults.")
|
log.Info().Msg("No benchmark config file found, using defaults.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ const (
|
||||||
splashTime = 1
|
splashTime = 1
|
||||||
clusterRefresh = time.Duration(5 * time.Second)
|
clusterRefresh = time.Duration(5 * time.Second)
|
||||||
statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
|
statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
|
||||||
|
clusterInfoWidth = 50
|
||||||
|
clusterInfoPad = 15
|
||||||
)
|
)
|
||||||
|
|
||||||
// App represents an application view.
|
// App represents an application view.
|
||||||
|
|
@ -136,7 +138,16 @@ func (a *App) buildHeader() tview.Primitive {
|
||||||
if !a.showHeader {
|
if !a.showHeader {
|
||||||
return header
|
return header
|
||||||
}
|
}
|
||||||
header.AddItem(a.clusterInfo(), 40, 1, false)
|
|
||||||
|
clWidth := clusterInfoWidth
|
||||||
|
n, err := a.Conn().Config().CurrentClusterName()
|
||||||
|
if err == nil {
|
||||||
|
size := len(n) + clusterInfoPad
|
||||||
|
if size > clWidth {
|
||||||
|
clWidth = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header.AddItem(a.clusterInfo(), clWidth, 1, false)
|
||||||
header.AddItem(a.Menu(), 0, 1, false)
|
header.AddItem(a.Menu(), 0, 1, false)
|
||||||
header.AddItem(a.Logo(), 26, 1, false)
|
header.AddItem(a.Logo(), 26, 1, false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ func (n *Node) bindKeys(aa ui.KeyActions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) nodeContext(ctx context.Context) context.Context {
|
func (n *Node) nodeContext(ctx context.Context) context.Context {
|
||||||
|
if !n.App().Conn().HasMetrics() {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
mx := client.NewMetricsServer(n.App().factory.Client())
|
mx := client.NewMetricsServer(n.App().factory.Client())
|
||||||
nmx, err := mx.FetchNodesMetrics()
|
nmx, err := mx.FetchNodesMetrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ func (f *Factory) Terminate() {
|
||||||
|
|
||||||
// List returns a resource collection.
|
// List returns a resource collection.
|
||||||
func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) {
|
func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) {
|
||||||
|
defer func(t time.Time) {
|
||||||
|
log.Debug().Msgf("FACTORY-LIST %q::%q elapsed %v", ns, gvr, time.Since(t))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
if ns == clusterScope {
|
if ns == clusterScope {
|
||||||
ns = allNamespaces
|
ns = allNamespaces
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue