add ns checks, percentage + bugz fixes

mine
derailed 2019-05-24 16:25:26 -06:00
parent 0cb1a9766f
commit 21ed022434
43 changed files with 746 additions and 428 deletions

View File

@ -9,17 +9,17 @@ builds:
- env: - env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:
# - linux - linux
# - darwin - darwin
- windows - windows
goarch: goarch:
# - 386 - 386
- amd64 - amd64
# - arm - arm
# - arm64 - arm64
# goarm: goarm:
# - 6 - 6
# - 7 - 7
ldflags: 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}} - -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}}
archive: archive:
@ -59,33 +59,34 @@ brew:
system "k9s version" system "k9s version"
# Snapcraft # Snapcraft
# snapcraft: snapcraft:
# name: k9s name: k9s
# summary: K9s is a CLI to view and manage your Kubernetes clusters. summary: K9s is a CLI to view and manage your Kubernetes clusters.
# description: | description: |
# K9s is a CLI to view and manage your Kubernetes clusters. K9s is a CLI to view and manage your Kubernetes clusters.
# By leveraging a terminal UI, you can easily traverse Kubernetes resources By leveraging a terminal UI, you can easily traverse Kubernetes resources
# and view the state of you clusters in a single powerful session. and view the state of you clusters in a single powerful session.
# name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
# publish: false # publish: false
# # publish: true publish: true
# replacements: replacements:
# amd64: 64-bit amd64: 64-bit
# 386: 32-bit 386: 32-bit
# darwin: macOS darwin: macOS
# linux: Tux linux: Tux
# bit: Arm bit: Arm
# bitv6: Arm6 bitv6: Arm6
# bitv7: Arm7 bitv7: Arm7
# grade: devel grade: devel
# confinement: devmode confinement: devmode
# # grade: stable # grade: stable
# # confinement: strict # confinement: strict
# apps: apps:
# k9s: k9s:
# plugs: ["home", "network"] # plugs: ["home", "network"]
# # plugs: ["home", "network", "personal-files"] plugs: ["home", "network", "kube-config"]
# plugs: plugs:
# personal-files: kube-config:
# read: interface: personal-files
# - $HOME/.kube read:
- $HOME/.kube

View File

@ -12,7 +12,7 @@ for changes and offers subsequent commands to interact with observed resources.
[![Go Report Card](https://goreportcard.com/badge/github.com/derailed/k9s?)](https://goreportcard.com/report/github.com/derailed/k9s) [![Go Report Card](https://goreportcard.com/badge/github.com/derailed/k9s?)](https://goreportcard.com/report/github.com/derailed/k9s)
[![Build Status](https://travis-ci.com/derailed/k9s.svg?branch=master)](https://travis-ci.com/derailed/k9s) [![Build Status](https://travis-ci.com/derailed/k9s.svg?branch=master)](https://travis-ci.com/derailed/k9s)
[![release](https://img.shields.io/github/release-pre/derailed/k9s.svg)](https://github.com/derailed/k9s/releases) [![release](https://img.shields.io/github/release-pre/derailed/k9s.svg)](https://github.com/derailed/k9s/releases)
[![k9s](https://snapcraft.io/k9s/badge.svg)](https://snapcraft.io/k9s) <!-- [![k9s](https://snapcraft.io/k9s/badge.svg)](https://snapcraft.io/k9s) -->
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)
--- ---

View File

@ -0,0 +1,39 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.6.7
## Notes
Thank you to all that contributed with flushing out issues with 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.
Thank you so much for your support and awesome suggestions to make K9s better!!
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
---
## Change Logs
This is a maintenance release to mainly resolve outstanding issues and bugs.
### Tracking Percentages
Added two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node views.
---
## Resolved Bugs
+ [Issue #192](https://github.com/derailed/k9s/issues/192)
+ [Issue #190](https://github.com/derailed/k9s/issues/190)
+ [Issue #189](https://github.com/derailed/k9s/issues/189)
+ [Issue #185](https://github.com/derailed/k9s/issues/185)
+ [Issue #171](https://github.com/derailed/k9s/issues/171)
+ [Issue #155](https://github.com/derailed/k9s/issues/155)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"flag"
"fmt" "fmt"
"runtime/debug" "runtime/debug"
@ -12,6 +13,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/klog"
) )
const ( const (
@ -41,6 +43,13 @@ func init() {
rootCmd.AddCommand(versionCmd(), infoCmd()) rootCmd.AddCommand(versionCmd(), infoCmd())
initK9sFlags() initK9sFlags()
initK8sFlags() initK8sFlags()
// Klogs (of course) want to print stuff to the screen ;(
klog.InitFlags(nil)
flag.Set("log_file", config.K9sLogs)
flag.Set("stderrthreshold", "fatal")
flag.Set("alsologtostderr", "false")
flag.Set("logtostderr", "false")
} }
// Execute root command // Execute root command

3
go.mod
View File

@ -4,7 +4,9 @@ go 1.12
replace ( replace (
k8s.io/api => k8s.io/api v0.0.0-20190222213804-5cb15d344471 k8s.io/api => k8s.io/api v0.0.0-20190222213804-5cb15d344471
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
k8s.io/apiserver => k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190325194458-f2b4781c3ae1 k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190325194458-f2b4781c3ae1
k8s.io/client-go => k8s.io/client-go v10.0.0+incompatible k8s.io/client-go => k8s.io/client-go v10.0.0+incompatible
k8s.io/metrics => k8s.io/metrics v0.0.0-20190325194013-29123f6a4aa6 k8s.io/metrics => k8s.io/metrics v0.0.0-20190325194013-29123f6a4aa6
@ -26,6 +28,7 @@ require (
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/imdario/mergo v0.3.7 // indirect github.com/imdario/mergo v0.3.7 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.6 // indirect github.com/json-iterator/go v1.1.6 // indirect
github.com/mattn/go-runewidth v0.0.4 github.com/mattn/go-runewidth v0.0.4
github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/ginkgo v1.8.0 // indirect

4
go.sum
View File

@ -362,10 +362,14 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE= k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE=
k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f h1:+GpMltIq6SUOswgSQ3HcxgldikyBCreeRDkCYOzwfGk=
k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0 h1:blst2tV97kE1/Mxaxx3zzh6zUGpxCbGNq0CdFf9/N8s= k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0 h1:blst2tV97kE1/Mxaxx3zzh6zUGpxCbGNq0CdFf9/N8s=
k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0/go.mod h1:IPM+7P9C3mY4uik+2wHMNbydKfSZpl9Hnu0Ze0447Wg= k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0/go.mod h1:IPM+7P9C3mY4uik+2wHMNbydKfSZpl9Hnu0Ze0447Wg=
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg= k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg=
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe h1:zD63Eo0qbcR9JzZ90yQsFMzXYSbfsCa5ICB2D2nX1tg=
k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w=
k8s.io/apiserver v0.0.0-20190426012941-33871ad74f4b/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80= k8s.io/apiserver v0.0.0-20190426012941-33871ad74f4b/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80=
k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716 h1:gByi/idNjfDDk+lWNRqWk2uE1/KAsJtYXRMEc2M1a1k= k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716 h1:gByi/idNjfDDk+lWNRqWk2uE1/KAsJtYXRMEc2M1a1k=
k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80= k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80=

View File

@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
return ret0, ret1 return ret0, ret1
} }
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) { func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
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{_param0, _param1} params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string var ret0 string
var ret1 bool var ret1 bool
var ret2 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(string) ret0 = result[0].(string)
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
if result[1] != nil { if result[1] != nil {
ret1 = result[1].(bool) ret1 = result[1].(bool)
} }
if result[2] != nil {
ret2 = result[2].(error)
}
} }
return ret0, ret1 return ret0, ret1, ret2
} }
func (mock *MockConnection) SupportsResource(_param0 string) bool { func (mock *MockConnection) SupportsResource(_param0 string) bool {

View File

@ -176,7 +176,7 @@ func newTableHeader() *TableHeader {
return &TableHeader{ return &TableHeader{
FgColor: "white", FgColor: "white",
BgColor: "black", BgColor: "black",
SorterColor: "orange", SorterColor: "aqua",
} }
} }

View File

@ -3,6 +3,7 @@ package k8s
import ( import (
"fmt" "fmt"
"strings" "strings"
"sync"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -59,7 +60,7 @@ type (
SupportsResource(group string) bool SupportsResource(group string) bool
ValidNamespaces() ([]v1.Namespace, error) ValidNamespaces() ([]v1.Namespace, error)
NodePods(node string) (*v1.PodList, error) NodePods(node string) (*v1.PodList, error)
SupportsRes(grp string, versions []string) (string, bool) SupportsRes(grp string, versions []string) (string, bool, error)
ServerVersion() (*version.Info, error) ServerVersion() (*version.Info, error)
FetchNodes() (*v1.NodeList, error) FetchNodes() (*v1.NodeList, error)
CurrentNamespaceName() (string, error) CurrentNamespaceName() (string, error)
@ -75,6 +76,7 @@ type (
mxsClient *versioned.Clientset mxsClient *versioned.Clientset
useMetricServer bool useMetricServer bool
log zerolog.Logger log zerolog.Logger
mx sync.Mutex
} }
) )
@ -104,6 +106,7 @@ func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) bool {
var resp *authorizationv1.SelfSubjectAccessReview var resp *authorizationv1.SelfSubjectAccessReview
var err error var err error
var allow bool
for _, v := range verbs { for _, v := range verbs {
sar.Spec.ResourceAttributes.Verb = v sar.Spec.ResourceAttributes.Verb = v
resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar) resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
@ -111,9 +114,14 @@ func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) bool {
log.Warn().Err(err).Msgf("CanIAccess") log.Warn().Err(err).Msgf("CanIAccess")
return false return false
} }
log.Debug().Msgf("CHECKING ACCESS for %s/%s/ in NS %q verb: %s -> %t, %s", resURL, name, ns, v, resp.Status.Allowed, resp.Status.Reason)
if !resp.Status.Allowed {
return false
}
allow = true
} }
log.Debug().Msgf("GRANT ACCESS:%t", allow)
return resp.Status.Allowed return allow
} }
// CurrentNamespaceName return namespace name set via either cli arg or cluster config. // CurrentNamespaceName return namespace name set via either cli arg or cluster config.
@ -230,28 +238,34 @@ func (a *APIClient) DynDialOrDie() dynamic.Interface {
// NSDialOrDie returns a handle to a namespaced resource. // NSDialOrDie returns a handle to a namespaced resource.
func (a *APIClient) NSDialOrDie() dynamic.NamespaceableResourceInterface { func (a *APIClient) NSDialOrDie() dynamic.NamespaceableResourceInterface {
a.mx.Lock()
defer a.mx.Unlock()
if a.nsClient != nil { if a.nsClient != nil {
return a.nsClient return a.nsClient
} }
a.nsClient = a.DynDialOrDie().Resource(schema.GroupVersionResource{ a.nsClient = a.DynDialOrDie().Resource(schema.GroupVersionResource{
Group: "apiextensions.k8s.io", Group: "apiextensions.k8s.io",
Version: "v1beta1", Version: "v1beta1",
Resource: "customresourcedefinitions", Resource: "customresourcedefinitions",
}) })
return a.nsClient return a.nsClient
} }
// MXDial returns a handle to the metrics server. // MXDial returns a handle to the metrics server.
func (a *APIClient) MXDial() (*versioned.Clientset, error) { func (a *APIClient) MXDial() (*versioned.Clientset, error) {
a.mx.Lock()
defer a.mx.Unlock()
if a.mxsClient != nil { if a.mxsClient != nil {
return a.mxsClient, nil return a.mxsClient, nil
} }
var err error var err error
if a.mxsClient, err = versioned.NewForConfig(a.RestConfigOrDie()); err != nil { if a.mxsClient, err = versioned.NewForConfig(a.RestConfigOrDie()); err != nil {
a.log.Debug().Err(err) a.log.Debug().Err(err)
} }
return a.mxsClient, err return a.mxsClient, err
} }
@ -299,27 +313,18 @@ func (a *APIClient) supportsMxServer() bool {
} }
// SupportsRes checks latest supported version. // SupportsRes checks latest supported version.
func (a *APIClient) SupportsRes(group string, versions []string) (string, bool) { func (a *APIClient) SupportsRes(group string, versions []string) (string, bool, error) {
apiGroups, err := a.DialOrDie().Discovery().ServerGroups() apiGroups, err := a.DialOrDie().Discovery().ServerGroups()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Unable to dial api groups") return "", false, err
return "", false
} }
for _, grp := range apiGroups.Groups { for _, grp := range apiGroups.Groups {
if grp.Name != group { if grp.Name != group {
continue continue
} }
return grp.PreferredVersion.Version, true return grp.PreferredVersion.Version, true, nil
// for _, version := range grp.Versions {
// for _, supportedVersion := range versions {
// if version.Version == supportedVersion {
// return supportedVersion, true
// }
// }
// }
} }
return "", false return "", false, nil
} }

View File

@ -147,20 +147,17 @@ func (r *Container) List(ns string) (Columnars, error) {
// Header return resource header. // Header return resource header.
func (*Container) Header(ns string) Row { func (*Container) Header(ns string) Row {
hh := Row{} return append(Row{},
return append(hh,
"NAME", "NAME",
"IMAGE", "IMAGE",
"READY", "READY",
"STATE", "STATE",
"RS", "RS",
"LPROB", "PROBES(L:R)",
"RPROB",
"CPU", "CPU",
"MEM", "MEM",
"RCPU", "%CPU",
"RMEM", "%MEM",
"AGE", "AGE",
) )
} }
@ -170,7 +167,7 @@ func (r *Container) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns))) ff := make(Row, 0, len(r.Header(ns)))
i := r.instance i := r.instance
scpu, smem := NAValue, NAValue scpu, smem, pcpu, pmem := NAValue, NAValue, NAValue, NAValue
if r.metrics != nil { if r.metrics != nil {
var ( var (
cpu int64 cpu int64
@ -184,8 +181,14 @@ func (r *Container) Fields(ns string) Row {
} }
} }
scpu, smem = ToMillicore(cpu), ToMi(mem) scpu, smem = ToMillicore(cpu), ToMi(mem)
rcpu, rmem := containerResources(i)
if rcpu != nil {
pcpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue())))
}
if rmem != nil {
pmem = AsPerc(toPerc(mem, k8s.ToMB(rmem.Value())))
}
} }
rcpu, rmem := resources(i)
var cs *v1.ContainerStatus var cs *v1.ContainerStatus
for _, c := range r.pod.Status.ContainerStatuses { for _, c := range r.pod.Status.ContainerStatuses {
@ -215,12 +218,11 @@ func (r *Container) Fields(ns string) Row {
ready, ready,
state, state,
restarts, restarts,
probe(i.LivenessProbe), probe(i.LivenessProbe)+":"+probe(i.ReadinessProbe),
probe(i.ReadinessProbe),
scpu, scpu,
smem, smem,
rcpu, pcpu,
rmem, pmem,
toAge(r.pod.CreationTimestamp), toAge(r.pod.CreationTimestamp),
) )
} }
@ -254,18 +256,6 @@ func toRes(r v1.ResourceList) (string, string) {
return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value())) return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value()))
} }
func resources(c v1.Container) (cpu, mem string) {
req, lim := c.Resources.Requests, c.Resources.Limits
if len(req) != 0 {
return toRes(req)
}
if len(lim) != 0 {
return toRes(lim)
}
return NAValue, NAValue
}
func probe(p *v1.Probe) string { func probe(p *v1.Probe) string {
if p == nil { if p == nil {
return "on" return "on"

View File

@ -81,7 +81,7 @@ func join(a []string, sep string) string {
// AsPerc prints a number as a percentage. // AsPerc prints a number as a percentage.
func AsPerc(f float64) string { func AsPerc(f float64) string {
return strconv.Itoa(int(f)) + "%" return strconv.Itoa(int(f))
} }
// ToPerc computes the ratio of two numbers as a percentage. // ToPerc computes the ratio of two numbers as a percentage.
@ -168,12 +168,12 @@ func mapToStr(m map[string]string) (s string) {
// ToMillicore shows cpu reading for human. // ToMillicore shows cpu reading for human.
func ToMillicore(v int64) string { func ToMillicore(v int64) string {
return strconv.Itoa(int(v)) + "m" return strconv.Itoa(int(v))
} }
// ToMi shows mem reading for human. // ToMi shows mem reading for human.
func ToMi(v float64) string { func ToMi(v float64) string {
return strconv.Itoa(int(v)) + "Mi" return strconv.Itoa(int(v))
} }
func boolPtrToStr(b *bool) string { func boolPtrToStr(b *bool) string {

View File

@ -142,9 +142,9 @@ func TestToMillicore(t *testing.T) {
v int64 v int64
e string e string
}{ }{
{0, "0m"}, {0, "0"},
{2, "2m"}, {2, "2"},
{1000, "1000m"}, {1000, "1000"},
} }
for _, u := range uu { for _, u := range uu {
@ -157,9 +157,9 @@ func TestToMi(t *testing.T) {
v float64 v float64
e string e string
}{ }{
{0, "0Mi"}, {0, "0"},
{2, "2Mi"}, {2, "2"},
{1000, "1000Mi"}, {1000, "1000"},
} }
for _, u := range uu { for _, u := range uu {
@ -172,10 +172,10 @@ func TestAsPerc(t *testing.T) {
v float64 v float64
e string e string
}{ }{
{0, "0%"}, {0, "0"},
{10.5, "10%"}, {10.5, "10"},
{10, "10%"}, {10, "10"},
{0.05, "0%"}, {0.05, "0"},
} }
for _, u := range uu { for _, u := range uu {

View File

@ -239,6 +239,7 @@ func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) {
LabelSelector: l.resource.GetLabelSelector(), LabelSelector: l.resource.GetLabelSelector(),
}) })
if err != nil { if err != nil {
log.Debug().Msgf(">>>>>> DOH! %#v", err)
return nil, err return nil, err
} }

View File

@ -288,14 +288,15 @@ func (mock *MockClusterMeta) ServerVersion() (*version.Info, error) {
return ret0, ret1 return ret0, ret1
} }
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool) { func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().") panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
} }
params := []pegomock.Param{_param0, _param1} params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string var ret0 string
var ret1 bool var ret1 bool
var ret2 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(string) ret0 = result[0].(string)
@ -303,8 +304,11 @@ func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (stri
if result[1] != nil { if result[1] != nil {
ret1 = result[1].(bool) ret1 = result[1].(bool)
} }
if result[2] != nil {
ret2 = result[2].(error)
}
} }
return ret0, ret1 return ret0, ret1, ret2
} }
func (mock *MockClusterMeta) SupportsResource(_param0 string) bool { func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {

View File

@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
return ret0, ret1 return ret0, ret1
} }
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) { func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
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{_param0, _param1} params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string var ret0 string
var ret1 bool var ret1 bool
var ret2 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(string) ret0 = result[0].(string)
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
if result[1] != nil { if result[1] != nil {
ret1 = result[1].(bool) ret1 = result[1].(bool)
} }
if result[2] != nil {
ret2 = result[2].(error)
}
} }
return ret0, ret1 return ret0, ret1, ret2
} }
func (mock *MockConnection) SupportsResource(_param0 string) bool { func (mock *MockConnection) SupportsResource(_param0 string) bool {

View File

@ -111,6 +111,8 @@ func (*Node) Header(ns string) Row {
"EXTERNAL-IP", "EXTERNAL-IP",
"CPU", "CPU",
"MEM", "MEM",
"%CPU",
"%MEM",
"ACPU", "ACPU",
"AMEM", "AMEM",
"AGE", "AGE",
@ -125,7 +127,7 @@ func (r *Node) Fields(ns string) Row {
iIP, eIP := r.getIPs(no.Status.Addresses) iIP, eIP := r.getIPs(no.Status.Addresses)
iIP, eIP = missing(iIP), missing(eIP) iIP, eIP = missing(iIP), missing(eIP)
ccpu, cmem, scpu, smem := NAValue, NAValue, NAValue, NAValue ccpu, cmem, scpu, smem, pcpu, pmem := NAValue, NAValue, NAValue, NAValue, NAValue, NAValue
if r.metrics != nil { if r.metrics != nil {
var ( var (
cpu int64 cpu int64
@ -137,8 +139,10 @@ func (r *Node) Fields(ns string) Row {
acpu := no.Status.Allocatable.Cpu().MilliValue() acpu := no.Status.Allocatable.Cpu().MilliValue()
amem := k8s.ToMB(no.Status.Allocatable.Memory().Value()) amem := k8s.ToMB(no.Status.Allocatable.Memory().Value())
ccpu = withPerc(ToMillicore(cpu), AsPerc(toPerc(float64(cpu), float64(acpu)))) ccpu = ToMillicore(cpu)
cmem = withPerc(ToMi(mem), AsPerc(toPerc(mem, amem))) pcpu = AsPerc(toPerc(float64(cpu), float64(acpu)))
cmem = ToMi(mem)
pmem = AsPerc(toPerc(mem, amem))
scpu = ToMillicore(cpu) scpu = ToMillicore(cpu)
smem = ToMi(mem) smem = ToMi(mem)
} }
@ -158,6 +162,8 @@ func (r *Node) Fields(ns string) Row {
eIP, eIP,
ccpu, ccpu,
cmem, cmem,
pcpu,
pmem,
scpu, scpu,
smem, smem,
toAge(no.ObjectMeta.CreationTimestamp), toAge(no.ObjectMeta.CreationTimestamp),

View File

@ -81,7 +81,7 @@ func TestNodeListData(t *testing.T) {
assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
row, ok := td.Rows["fred"] row, ok := td.Rows["fred"]
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, 12, len(row.Deltas)) assert.Equal(t, 14, len(row.Deltas))
for _, d := range row.Deltas { for _, d := range row.Deltas {
assert.Equal(t, "", d) assert.Equal(t, "", d)
} }

View File

@ -62,21 +62,6 @@ func (r *Namespace) Marshal(path string) (string, error) {
return r.marshalObject(nss) return r.marshalObject(nss)
} }
// // List resources for a given namespace.
// func (r *Namespace) List(ns string) (Columnars, error) {
// r.Resource.
// nss, err := r.Resource.List(ns)
// if err != nil {
// return nil, err
// }
// cc := make(Columnars, 0, len(nss))
// for i := range nss {
// cc = append(cc, r.New(&nss[i]).(*Namespace))
// }
// return cc, nil
// }
// Header returns resource header. // Header returns resource header.
func (*Namespace) Header(ns string) Row { func (*Namespace) Header(ns string) Row {
return Row{"NAME", "STATUS", "AGE"} return Row{"NAME", "STATUS", "AGE"}

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/kubernetes/pkg/util/node" "k8s.io/kubernetes/pkg/util/node"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
) )
@ -198,6 +199,8 @@ func (*Pod) Header(ns string) Row {
"RS", "RS",
"CPU", "CPU",
"MEM", "MEM",
"%CPU",
"%MEM",
"IP", "IP",
"NODE", "NODE",
"QOS", "QOS",
@ -217,17 +220,13 @@ func (r *Pod) Fields(ns string) Row {
ss := i.Status.ContainerStatuses ss := i.Status.ContainerStatuses
cr, _, rc := r.statuses(ss) cr, _, rc := r.statuses(ss)
scpu, smem := NAValue, NAValue ccpu, cmem, pcpu, pmem := NAValue, NAValue, NAValue, NAValue
if r.metrics != nil { if r.metrics != nil {
var cpu int64 c, m := r.currentRes(r.metrics)
var mem float64 ccpu, cmem = ToMillicore(c.MilliValue()), ToMi(k8s.ToMB(m.Value()))
rc, rm := r.requestedRes(i)
for _, c := range r.metrics.Containers { pcpu = AsPerc(toPerc(float64(c.MilliValue()), float64(rc.MilliValue())))
cpu += c.Usage.Cpu().MilliValue() pmem = AsPerc(toPerc(k8s.ToMB(m.Value()), k8s.ToMB(rm.Value())))
mem += k8s.ToMB(c.Usage.Memory().Value())
}
scpu = ToMillicore(cpu)
smem = ToMi(mem)
} }
return append(ff, return append(ff,
@ -235,8 +234,10 @@ func (r *Pod) Fields(ns string) Row {
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)), strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
r.phase(i), r.phase(i),
strconv.Itoa(rc), strconv.Itoa(rc),
scpu, ccpu,
smem, cmem,
pcpu,
pmem,
i.Status.PodIP, i.Status.PodIP,
i.Spec.NodeName, i.Spec.NodeName,
r.mapQOS(i.Status.QOSClass), r.mapQOS(i.Status.QOSClass),
@ -247,6 +248,41 @@ func (r *Pod) Fields(ns string) Row {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func containerResources(co v1.Container) (cpu, mem *resource.Quantity) {
req, limit := co.Resources.Requests, co.Resources.Limits
switch {
case len(req) != 0 && len(limit) != 0:
cpu, mem = limit.Cpu(), limit.Memory()
case len(req) != 0:
cpu, mem = req.Cpu(), req.Memory()
case len(limit) != 0:
cpu, mem = limit.Cpu(), limit.Memory()
}
return
}
func (r *Pod) requestedRes(po *v1.Pod) (cpu, mem resource.Quantity) {
for _, co := range po.Spec.Containers {
c, m := containerResources(co)
if c != nil {
cpu.Add(*c)
}
if m != nil {
mem.Add(*m)
}
}
return
}
func (*Pod) currentRes(mx *mv1beta1.PodMetrics) (cpu, mem resource.Quantity) {
for _, co := range mx.Containers {
c, m := co.Usage.Cpu(), co.Usage.Memory()
cpu.Add(*c)
mem.Add(*m)
}
return
}
func (*Pod) mapQOS(class v1.PodQOSClass) string { func (*Pod) mapQOS(class v1.PodQOSClass) string {
switch class { switch class {
case v1.PodQOSGuaranteed: case v1.PodQOSGuaranteed:

View File

@ -79,7 +79,7 @@ func TestPodListData(t *testing.T) {
assert.Equal(t, 1, len(td.Rows)) assert.Equal(t, 1, len(td.Rows))
assert.Equal(t, "blee", l.GetNamespace()) assert.Equal(t, "blee", l.GetNamespace())
row := td.Rows["blee/fred"] row := td.Rows["blee/fred"]
assert.Equal(t, 10, len(row.Deltas)) assert.Equal(t, 12, len(row.Deltas))
for _, d := range row.Deltas { for _, d := range row.Deltas {
assert.Equal(t, "", d) assert.Equal(t, "", d)
} }

View File

@ -216,11 +216,14 @@ func (a *appView) Run() {
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key() key := evt.Key()
if key == tcell.KeyRune { if key == tcell.KeyRune {
if a.cmdBuff.isActive() { if a.cmdBuff.isActive() && evt.Modifiers() == tcell.ModNone {
a.cmdBuff.add(evt.Rune()) a.cmdBuff.add(evt.Rune())
return nil return nil
} }
key = tcell.Key(evt.Rune()) key = tcell.Key(evt.Rune())
if evt.Modifiers() == tcell.ModAlt {
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
}
} }
if a, ok := a.actions[key]; ok { if a, ok := a.actions[key]; ok {
@ -340,7 +343,6 @@ func (a *appView) inject(i igniter) {
a.cancel() a.cancel()
} }
a.content.RemovePage("main") a.content.RemovePage("main")
var ctx context.Context var ctx context.Context
{ {
ctx, a.cancel = context.WithCancel(context.Background()) ctx, a.cancel = context.WithCancel(context.Background())

View File

@ -124,16 +124,22 @@ func (v *clusterInfoView) refresh() {
cluster.Metrics(nos, nmx, &cmx) cluster.Metrics(nos, nmx, &cmx)
c = v.GetCell(row, 1) c = v.GetCell(row, 1)
cpu := resource.AsPerc(cmx.PercCPU) cpu := resource.AsPerc(cmx.PercCPU)
if cpu == "0" {
cpu = resource.NAValue
}
c.SetText(cpu + deltas(strip(c.Text), cpu)) c.SetText(cpu + deltas(strip(c.Text), cpu))
row++ row++
c = v.GetCell(row, 1) c = v.GetCell(row, 1)
mem := resource.AsPerc(cmx.PercMEM) mem := resource.AsPerc(cmx.PercMEM)
if mem == "0" {
mem = resource.NAValue
}
c.SetText(mem + deltas(strip(c.Text), mem)) c.SetText(mem + deltas(strip(c.Text), mem))
} }
func strip(s string) string { func strip(s string) string {
t := strings.Replace(s, plus(), "", 1) t := strings.Replace(s, plusSign, "", 1)
t = strings.Replace(t, minus(), "", 1) t = strings.Replace(t, minusSign, "", 1)
return t return t
} }

View File

@ -10,14 +10,16 @@ type containerView struct {
*resourceView *resourceView
current igniter current igniter
exitFn func()
} }
func newContainerView(t string, app *appView, list resource.List, path string) resourceViewer { func newContainerView(t string, app *appView, list resource.List, path string, exitFn func()) resourceViewer {
v := containerView{resourceView: newResourceView(t, app, list).(*resourceView)} v := containerView{resourceView: newResourceView(t, app, list).(*resourceView)}
{ {
v.path = &path v.path = &path
v.extraActionsFn = v.extraActions v.extraActionsFn = v.extraActions
v.current = app.content.GetPrimitive("main").(igniter) v.current = app.content.GetPrimitive("main").(igniter)
v.exitFn = exitFn
} }
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
v.switchPage("co") v.switchPage("co")
@ -97,13 +99,15 @@ func (v *containerView) shellIn(path, co string) {
func (v *containerView) extraActions(aa keyActions) { func (v *containerView) extraActions(aa keyActions) {
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true) aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false) aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false)
aa[KeyP] = newKeyAction("Previous", v.backCmd, false) aa[KeyP] = newKeyAction("Previous", v.backCmd, false)
aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false) aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false)
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true) aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, false), true)
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true) aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(7, false), true)
aa[KeyAltC] = newKeyAction("Sort %CPU", v.sortColCmd(8, false), true)
aa[KeyAltM] = newKeyAction("Sort %MEM", v.sortColCmd(9, false), true)
} }
func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
@ -117,7 +121,8 @@ func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey)
} }
func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.app.inject(v.current) // v.app.inject(v.current)
v.exitFn()
return nil return nil
} }

View File

@ -48,7 +48,27 @@ func (v *flashView) setMessage(level flashLevel, msg ...string) {
if v.cancel != nil { if v.cancel != nil {
v.cancel() v.cancel()
} }
var ctx1, ctx2 context.Context
{
ctx1, v.cancel = context.WithCancel(context.TODO())
ctx2, _ = context.WithTimeout(context.TODO(), flashDelay*time.Second)
go func(ctx1, ctx2 context.Context) {
for {
select {
// Timer canceled bail now
case <-ctx1.Done():
return
// Timed out clear and bail
case <-ctx2.Done():
v.app.QueueUpdateDraw(func() {
v.Clear()
v.app.Draw()
})
return
}
}
}(ctx1, ctx2)
}
_, _, width, _ := v.GetRect() _, _, width, _ := v.GetRect()
if width <= 15 { if width <= 15 {
width = 100 width = 100
@ -56,23 +76,6 @@ func (v *flashView) setMessage(level flashLevel, msg ...string) {
m := strings.Join(msg, " ") m := strings.Join(msg, " ")
v.SetTextColor(flashColor(level)) v.SetTextColor(flashColor(level))
v.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3)) v.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3))
var ctx context.Context
{
ctx, v.cancel = context.WithTimeout(context.TODO(), flashDelay*time.Second)
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
v.app.QueueUpdateDraw(func() {
v.Clear()
// v.app.Draw()
})
return
}
}
}(ctx)
}
} }
func flashEmoji(l flashLevel) string { func flashEmoji(l flashLevel) string {

View File

@ -10,6 +10,15 @@ import (
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
) )
const (
// deltaSign = "𝜟"
// plusSign = "⬆"
// minusSign = "⬇︎"
deltaSign = "Δ"
plusSign = "↑"
minusSign = "↓"
)
func deltas(o, n string) string { func deltas(o, n string) string {
o, n = strings.TrimSpace(o), strings.TrimSpace(n) o, n = strings.TrimSpace(o), strings.TrimSpace(n)
if o == "" || o == res.NAValue { if o == "" || o == res.NAValue {
@ -20,9 +29,9 @@ func deltas(o, n string) string {
j, _ := numerical(n) j, _ := numerical(n)
switch { switch {
case i < j: case i < j:
return plus() return plusSign
case i > j: case i > j:
return minus() return minusSign
default: default:
return "" return ""
} }
@ -32,9 +41,9 @@ func deltas(o, n string) string {
j, _ := percentage(n) j, _ := percentage(n)
switch { switch {
case i < j: case i < j:
return plus() return plusSign
case i > j: case i > j:
return minus() return minusSign
default: default:
return "" return ""
} }
@ -44,9 +53,9 @@ func deltas(o, n string) string {
q2, _ := resource.ParseQuantity(n) q2, _ := resource.ParseQuantity(n)
switch q1.Cmp(q2) { switch q1.Cmp(q2) {
case -1: case -1:
return plus() return plusSign
case 1: case 1:
return minus() return minusSign
default: default:
return "" return ""
} }
@ -56,9 +65,9 @@ func deltas(o, n string) string {
d2, _ := time.ParseDuration(n) d2, _ := time.ParseDuration(n)
switch { switch {
case d2-d1 > 0: case d2-d1 > 0:
return plus() return plusSign
case d2-d1 < 0: case d2-d1 < 0:
return minus() return minusSign
default: default:
return "" return ""
} }
@ -66,7 +75,7 @@ func deltas(o, n string) string {
switch strings.Compare(o, n) { switch strings.Compare(o, n) {
case 1, -1: case 1, -1:
return delta() return deltaSign
default: default:
return "" return ""
} }
@ -91,15 +100,3 @@ func numerical(s string) (int, bool) {
return n, true return n, true
} }
func delta() string {
return "𝜟"
}
func plus() string {
return "⬆"
}
func minus() string {
return "⬇︎"
}

View File

@ -17,22 +17,22 @@ func TestDeltas(t *testing.T) {
s1, s2, e string s1, s2, e string
}{ }{
{"", "", ""}, {"", "", ""},
{resource.MissingValue, "", delta()}, {resource.MissingValue, "", deltaSign},
{resource.NAValue, "", ""}, {resource.NAValue, "", ""},
{"fred", "fred", ""}, {"fred", "fred", ""},
{"fred", "blee", delta()}, {"fred", "blee", deltaSign},
{"1", "1", ""}, {"1", "1", ""},
{"1", "2", plus()}, {"1", "2", plusSign},
{"2", "1", minus()}, {"2", "1", minusSign},
{"2m33s", "2m33s", ""}, {"2m33s", "2m33s", ""},
{"2m33s", "1m", minus()}, {"2m33s", "1m", minusSign},
{"33s", "1m", plus()}, {"33s", "1m", plusSign},
{"10Gi", "10Gi", ""}, {"10Gi", "10Gi", ""},
{"10Gi", "20Gi", plus()}, {"10Gi", "20Gi", plusSign},
{"30Gi", "20Gi", minus()}, {"30Gi", "20Gi", minusSign},
{"15%", "15%", ""}, {"15%", "15%", ""},
{"20%", "40%", plus()}, {"20%", "40%", plusSign},
{"5%", "2%", minus()}, {"5%", "2%", minusSign},
} }
for _, u := range uu { for _, u := range uu {

View File

@ -104,7 +104,7 @@ func (v *jobView) showLogs(path, co, view string, parent loggable, prev bool) {
func (v *jobView) extraActions(aa keyActions) { func (v *jobView) extraActions(aa keyActions) {
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true) aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
} }

View File

@ -6,45 +6,154 @@ import (
"strings" "strings"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
) )
type logView struct { type logView struct {
*detailsView *tview.Flex
app *appView
logs *detailsView
status *statusView
parent masterView
ansiWriter io.Writer ansiWriter io.Writer
autoScroll bool
actions keyActions
} }
func newLogView(title string, parent loggable) *logView { func newLogView(title string, parent masterView) *logView {
v := logView{detailsView: newDetailsView(parent.appView(), parent.backFn())} v := logView{Flex: tview.NewFlex(), app: parent.appView()}
v.autoScroll = true
v.parent = parent
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
v.logs = newDetailsView(parent.appView(), parent.backFn())
{ {
v.SetBorderPadding(0, 0, 1, 1) v.logs.SetBorder(false)
v.setCategory("Logs") v.logs.setCategory("Logs")
v.SetDynamicColors(true) v.logs.SetDynamicColors(true)
v.SetWrap(true) v.logs.SetWrap(true)
v.setTitle(parent.getSelection()) v.logs.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize)
v.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize)
v.ansiWriter = tview.ANSIWriter(v)
} }
v.ansiWriter = tview.ANSIWriter(v.logs)
v.status = newStatusView(parent.appView())
v.SetDirection(tview.FlexRow)
v.AddItem(v.status, 1, 1, false)
v.AddItem(v.logs, 0, 1, true)
v.actions = keyActions{
tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true},
KeyC: {description: "Clear", action: v.clearCmd, visible: true},
KeyS: {description: "Toggle AutoScroll", action: v.toggleScrollCmd, visible: true},
KeyG: {description: "Top", action: v.topCmd, visible: false},
KeyShiftG: {description: "Bottom", action: v.bottomCmd, visible: false},
KeyF: {description: "Up", action: v.pageUpCmd, visible: false},
KeyB: {description: "Down", action: v.pageDownCmd, visible: false},
}
v.logs.SetInputCapture(v.keyboard)
return &v return &v
} }
func (l *logView) logLine(line string) { // Hints show action hints
fmt.Fprintln(l.ansiWriter, tview.Escape(line)) func (v *logView) hints() hints {
return v.actions.toHints()
} }
func (l *logView) log(lines fmt.Stringer) { func (v *logView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
l.Clear() key := evt.Key()
fmt.Fprintln(l.ansiWriter, lines.String()) if key == tcell.KeyRune {
key = tcell.Key(evt.Rune())
}
if m, ok := v.actions[key]; ok {
log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key])
return m.action(evt)
}
return evt
} }
func (l *logView) flush(index int, buff []string, scroll bool) { func (v *logView) logLine(line string) {
if index > 0 { fmt.Fprintln(v.ansiWriter, tview.Escape(line))
l.logLine(strings.Join(buff[:index], "\n")) }
if scroll {
l.app.QueueUpdate(func() { func (v *logView) log(lines fmt.Stringer) {
l.ScrollToEnd() v.logs.Clear()
}) fmt.Fprintln(v.ansiWriter, lines.String())
} }
func (v *logView) flush(index int, buff []string) {
if index == 0 {
return
}
v.logLine(strings.Join(buff[:index], "\n"))
if v.autoScroll {
v.app.QueueUpdateDraw(func() {
v.update()
v.logs.ScrollToEnd()
})
} }
} }
func (v *logView) update() {
status := "Off"
if v.autoScroll {
status = "On"
}
v.status.update([]string{fmt.Sprintf("Autoscroll: %s", status)})
}
// ----------------------------------------------------------------------------
// Actions...
func (v *logView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
v.autoScroll = !v.autoScroll
if v.autoScroll {
v.app.flash(flashInfo, "Autoscroll is on.")
v.logs.ScrollToEnd()
} else {
v.logs.PageUp()
v.app.flash(flashInfo, "Autoscroll is off.")
}
v.update()
return nil
}
func (v *logView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
return v.parent.backFn()(evt)
}
func (v *logView) topCmd(evt *tcell.EventKey) *tcell.EventKey {
v.app.flash(flashInfo, "Top of logs...")
v.logs.ScrollToBeginning()
return nil
}
func (v *logView) bottomCmd(*tcell.EventKey) *tcell.EventKey {
v.app.flash(flashInfo, "Bottom of logs...")
v.logs.ScrollToEnd()
return nil
}
func (v *logView) pageUpCmd(*tcell.EventKey) *tcell.EventKey {
if v.logs.PageUp() {
v.app.flash(flashInfo, "Reached Top ...")
}
return nil
}
func (v *logView) pageDownCmd(*tcell.EventKey) *tcell.EventKey {
if v.logs.PageDown() {
v.app.flash(flashInfo, "Reached Bottom ...")
}
return nil
}
func (v *logView) clearCmd(*tcell.EventKey) *tcell.EventKey {
v.app.flash(flashInfo, "Clearing logs...")
v.logs.Clear()
v.logs.ScrollTo(0, 0)
return nil
}

View File

@ -3,7 +3,7 @@ package views
import ( import (
"context" "context"
"fmt" "fmt"
"strconv" "strings"
"time" "time"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
@ -20,6 +20,11 @@ const (
flushTimeout = 200 * time.Millisecond flushTimeout = 200 * time.Millisecond
) )
type masterView interface {
backFn() actionHandler
appView() *appView
}
type logsView struct { type logsView struct {
*tview.Pages *tview.Pages
@ -28,7 +33,6 @@ type logsView struct {
containers []string containers []string
actions keyActions actions keyActions
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
autoScroll bool
showPrevious bool showPrevious bool
} }
@ -37,19 +41,8 @@ func newLogsView(pview string, parent loggable) *logsView {
Pages: tview.NewPages(), Pages: tview.NewPages(),
parent: parent, parent: parent,
parentView: pview, parentView: pview,
autoScroll: true,
containers: []string{}, containers: []string{},
} }
v.setActions(keyActions{
tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true},
KeyC: {description: "Clear", action: v.clearCmd, visible: true},
KeyS: {description: "Toggle AutoScroll", action: v.toggleScrollCmd, visible: true},
KeyG: {description: "Top", action: v.topCmd, visible: false},
KeyShiftG: {description: "Bottom", action: v.bottomCmd, visible: false},
KeyF: {description: "Up", action: v.pageUpCmd, visible: false},
KeyB: {description: "Down", action: v.pageDownCmd, visible: false},
})
v.SetInputCapture(v.keyboard)
return &v return &v
} }
@ -63,35 +56,6 @@ func (v *logsView) reload(co string, parent loggable, view string, prevLogs bool
v.load(0) v.load(0)
} }
func (v *logsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key()
if key == tcell.KeyRune {
key = tcell.Key(evt.Rune())
}
if kv, ok := v.CurrentPage().Item.(keyHandler); ok {
if kv.keyboard(evt) == nil {
return nil
}
}
if evt.Key() == tcell.KeyRune {
if i, err := strconv.Atoi(string(evt.Rune())); err == nil {
if _, ok := numKeys[i]; ok {
v.load(i - 1)
return nil
}
}
}
if m, ok := v.actions[key]; ok {
log.Debug().Msgf(">> LogsView handled %s", tcell.KeyNames[key])
return m.action(evt)
}
return evt
}
// SetActions to handle keyboard events. // SetActions to handle keyboard events.
func (v *logsView) setActions(aa keyActions) { func (v *logsView) setActions(aa keyActions) {
v.actions = aa v.actions = aa
@ -99,25 +63,24 @@ func (v *logsView) setActions(aa keyActions) {
// Hints show action hints // Hints show action hints
func (v *logsView) hints() hints { func (v *logsView) hints() hints {
if len(v.containers) > 1 { l := v.CurrentPage().Item.(*logView)
for i, c := range v.containers { return l.actions.toHints()
v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil, true)
}
}
return v.actions.toHints()
} }
func (v *logsView) addContainer(n string) { func (v *logsView) addContainer(n string) {
v.containers = append(v.containers, n) v.containers = append(v.containers, n)
l := newLogView(n, v.parent) l := newLogView(n, v)
{
l.SetInputCapture(v.keyboard)
l.backFn = v.backCmd
}
v.AddPage(n, l, true, false) v.AddPage(n, l, true, false)
} }
func (v *logsView) appView() *appView {
return v.parent.appView()
}
func (v *logsView) backFn() actionHandler {
return v.backCmd
}
func (v *logsView) deleteAllPages() { func (v *logsView) deleteAllPages() {
for i, c := range v.containers { for i, c := range v.containers {
v.RemovePage(c) v.RemovePage(c)
@ -130,7 +93,6 @@ func (v *logsView) stop() {
if v.cancelFunc == nil { if v.cancelFunc == nil {
return return
} }
v.cancelFunc() v.cancelFunc()
log.Debug().Msgf("Canceling logs...") log.Debug().Msgf("Canceling logs...")
v.cancelFunc = nil v.cancelFunc = nil
@ -140,7 +102,6 @@ func (v *logsView) load(i int) {
if i < 0 || i > len(v.containers)-1 { if i < 0 || i > len(v.containers)-1 {
return return
} }
v.SwitchToPage(v.containers[i]) v.SwitchToPage(v.containers[i])
if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil { if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
v.parent.appView().flash(flashErr, err.Error()) v.parent.appView().flash(flashErr, err.Error())
@ -156,8 +117,12 @@ func (v *logsView) doLoad(path, co string) error {
maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize) maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize)
l := v.CurrentPage().Item.(*logView) l := v.CurrentPage().Item.(*logView)
l.Clear() l.logs.Clear()
l.setTitle(path + ":" + co) const logFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:-:-]) "
fmat := fmt.Sprintf(logFmt, path, co)
fmat = strings.Replace(fmat, "[fg:bg", "["+v.parent.appView().styles.Style.Title.FgColor+":"+v.parent.appView().styles.Style.Title.BgColor, -1)
fmat = strings.Replace(fmat, "[hilite", "["+v.parent.appView().styles.Style.Title.HighlightColor, 1)
l.SetTitle(fmat)
c := make(chan string, 10) c := make(chan string, 10)
go func(l *logView) { go func(l *logView) {
@ -166,7 +131,7 @@ func (v *logsView) doLoad(path, co string) error {
select { select {
case line, ok := <-c: case line, ok := <-c:
if !ok { if !ok {
l.flush(index, buff, v.autoScroll) l.flush(index, buff)
index = 0 index = 0
return return
} }
@ -175,11 +140,11 @@ func (v *logsView) doLoad(path, co string) error {
index++ index++
continue continue
} }
l.flush(index, buff, v.autoScroll) l.flush(index, buff)
index = 0 index = 0
buff[index] = line buff[index] = line
case <-time.After(flushTimeout): case <-time.After(flushTimeout):
l.flush(index, buff, v.autoScroll) l.flush(index, buff)
index = 0 index = 0
} }
} }
@ -204,67 +169,9 @@ func (v *logsView) doLoad(path, co string) error {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Actions... // Actions...
func (v *logsView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
v.autoScroll = !v.autoScroll
if v.autoScroll {
v.parent.appView().flash(flashInfo, "Autoscroll is on.")
} else {
v.parent.appView().flash(flashInfo, "Autoscroll is off.")
}
return nil
}
func (v *logsView) backCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *logsView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.stop() v.stop()
v.parent.switchPage(v.parentView) v.parent.switchPage(v.parentView)
return evt return evt
} }
func (v *logsView) topCmd(evt *tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Top of logs...")
p.Item.(*logView).ScrollToBeginning()
}
return nil
}
func (v *logsView) bottomCmd(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Bottom of logs...")
p.Item.(*logView).ScrollToEnd()
}
return nil
}
func (v *logsView) pageUpCmd(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil {
if p.Item.(*logView).PageUp() {
v.parent.appView().flash(flashInfo, "Reached Top ...")
}
}
return nil
}
func (v *logsView) pageDownCmd(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil {
if p.Item.(*logView).PageDown() {
v.parent.appView().flash(flashInfo, "Reached Bottom ...")
}
}
return nil
}
func (v *logsView) clearCmd(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Clearing logs...")
p.Item.(*logView).Clear()
}
return nil
}

View File

@ -207,6 +207,36 @@ const (
Key9 Key9
) )
// Defines AltKeys
const (
KeyAltA tcell.Key = 4 * (iota + 97)
KeyAltB
KeyAltC
KeyAltD
KeyAltE
KeyAltF
KeyAltG
KeyAltH
KeyAltI
KeyAltJ
KeyAltK
KeyAltL
KeyAltM
KeyAltN
KeyAltO
KeyAltP
KeyAltQ
KeyAltR
KeyAltS
KeyAltT
KeyAltU
KeyAltV
KeyAltW
KeyAltX
KeyAltY
KeyAltZ
)
// Defines char keystrokes // Defines char keystrokes
const ( const (
KeyA tcell.Key = iota + 97 KeyA tcell.Key = iota + 97
@ -351,4 +381,32 @@ func initKeys() {
tcell.KeyNames[tcell.Key(KeyHelp)] = "?" tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
tcell.KeyNames[tcell.Key(KeySlash)] = "/" tcell.KeyNames[tcell.Key(KeySlash)] = "/"
tcell.KeyNames[tcell.Key(KeyAltA)] = "ALT-A"
tcell.KeyNames[tcell.Key(KeyAltB)] = "ALT-B"
tcell.KeyNames[tcell.Key(KeyAltC)] = "ALT-C"
tcell.KeyNames[tcell.Key(KeyAltD)] = "ALT-D"
tcell.KeyNames[tcell.Key(KeyAltE)] = "ALT-E"
tcell.KeyNames[tcell.Key(KeyAltF)] = "ALT-F"
tcell.KeyNames[tcell.Key(KeyAltG)] = "ALT-G"
tcell.KeyNames[tcell.Key(KeyAltH)] = "ALT-H"
tcell.KeyNames[tcell.Key(KeyAltI)] = "ALT-I"
tcell.KeyNames[tcell.Key(KeyAltJ)] = "ALT-J"
tcell.KeyNames[tcell.Key(KeyAltK)] = "ALT-K"
tcell.KeyNames[tcell.Key(KeyAltL)] = "ALT-L"
tcell.KeyNames[tcell.Key(KeyAltM)] = "ALT-M"
tcell.KeyNames[tcell.Key(KeyAltN)] = "ALT-N"
tcell.KeyNames[tcell.Key(KeyAltO)] = "ALT-O"
tcell.KeyNames[tcell.Key(KeyAltP)] = "ALT-P"
tcell.KeyNames[tcell.Key(KeyAltQ)] = "ALT-Q"
tcell.KeyNames[tcell.Key(KeyAltR)] = "ALT-R"
tcell.KeyNames[tcell.Key(KeyAltS)] = "ALT-S"
tcell.KeyNames[tcell.Key(KeyAltT)] = "ALT-T"
tcell.KeyNames[tcell.Key(KeyAltU)] = "ALT-U"
tcell.KeyNames[tcell.Key(KeyAltV)] = "ALT-V"
tcell.KeyNames[tcell.Key(KeyAltW)] = "ALT-W"
tcell.KeyNames[tcell.Key(KeyAltX)] = "ALT-X"
tcell.KeyNames[tcell.Key(KeyAltY)] = "ALT-Y"
tcell.KeyNames[tcell.Key(KeyAltZ)] = "ALT-Z"
} }

View File

@ -288,14 +288,15 @@ func (mock *MockClusterMeta) ServerVersion() (*version.Info, error) {
return ret0, ret1 return ret0, ret1
} }
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool) { func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().") panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
} }
params := []pegomock.Param{_param0, _param1} params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string var ret0 string
var ret1 bool var ret1 bool
var ret2 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(string) ret0 = result[0].(string)
@ -303,8 +304,11 @@ func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (stri
if result[1] != nil { if result[1] != nil {
ret1 = result[1].(bool) ret1 = result[1].(bool)
} }
if result[2] != nil {
ret2 = result[2].(error)
}
} }
return ret0, ret1 return ret0, ret1, ret2
} }
func (mock *MockClusterMeta) SupportsResource(_param0 string) bool { func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {

View File

@ -1,6 +1,7 @@
package views package views
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
@ -17,6 +18,8 @@ const containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
type podView struct { type podView struct {
*resourceView *resourceView
cancel context.CancelFunc
} }
type loggable interface { type loggable interface {
@ -33,7 +36,6 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer {
v.extraActionsFn = v.extraActions v.extraActionsFn = v.extraActions
v.enterFn = v.listContainers v.enterFn = v.listContainers
} }
picker := newSelectList(&v) picker := newSelectList(&v)
{ {
picker.setActions(keyActions{ picker.setActions(keyActions{
@ -41,7 +43,6 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer {
}) })
} }
v.AddPage("picker", picker, true, false) v.AddPage("picker", picker, true, false)
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
v.switchPage("po") v.switchPage("po")
@ -52,14 +53,12 @@ func (v *podView) listContainers(app *appView, _, res, sel string) {
if !v.rowSelected() { if !v.rowSelected() {
return return
} }
po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{}) po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{})
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel) log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel)
app.flash(flashErr, err.Error()) app.flash(flashErr, err.Error())
return return
} }
pod := po.(*v1.Pod) pod := po.(*v1.Pod)
mx := k8s.NewMetricsServer(app.conn()) mx := k8s.NewMetricsServer(app.conn())
list := resource.NewContainerList(app.conn(), mx, pod) list := resource.NewContainerList(app.conn(), mx, pod)
@ -68,12 +67,20 @@ func (v *podView) listContainers(app *appView, _, res, sel string) {
fmat := strings.Replace(containerFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1) fmat := strings.Replace(containerFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1) fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
title := fmt.Sprintf(fmat, "Containers", sel) title := fmt.Sprintf(fmat, "Containers", sel)
app.inject(newContainerView(
title, v.suspend()
app, cv := newContainerView(title, app, list, namespacedName(pod.Namespace, pod.Name), v.exitFn)
list, v.AddPage("containers", cv, true, true)
namespacedName(pod.Namespace, pod.Name), ctx, cancel := context.WithCancel(context.Background())
)) v.cancel = cancel
cv.init(ctx, pod.Namespace)
}
func (v *podView) exitFn() {
v.cancel()
v.switchPage("po")
v.RemovePage("containers")
v.resume()
} }
// Protocol... // Protocol...
@ -116,19 +123,16 @@ func (v *podView) viewLogs(prev bool) bool {
if !v.rowSelected() { if !v.rowSelected() {
return false return false
} }
cc, err := fetchContainers(v.list, v.selectedItem, true) cc, err := fetchContainers(v.list, v.selectedItem, true)
if err != nil { if err != nil {
v.app.flash(flashErr, err.Error()) v.app.flash(flashErr, err.Error())
log.Error().Err(err) log.Error().Err(err)
return false return false
} }
if len(cc) == 1 { if len(cc) == 1 {
v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, prev) v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, prev)
return true return true
} }
picker := v.GetPrimitive("picker").(*selectList) picker := v.GetPrimitive("picker").(*selectList)
picker.populate(cc) picker.populate(cc)
picker.SetSelectedFunc(func(i int, t, d string, r rune) { picker.SetSelectedFunc(func(i int, t, d string, r rune) {
@ -142,7 +146,6 @@ func (v *podView) viewLogs(prev bool) bool {
func (v *podView) showLogs(path, co, view string, parent loggable, prev bool) { func (v *podView) showLogs(path, co, view string, parent loggable, prev bool) {
l := v.GetPrimitive("logs").(*logsView) l := v.GetPrimitive("logs").(*logsView)
l.reload(co, parent, view, prev) l.reload(co, parent, view, prev)
v.switchPage("logs") v.switchPage("logs")
} }
@ -150,19 +153,16 @@ func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return evt return evt
} }
cc, err := fetchContainers(v.list, v.selectedItem, false) cc, err := fetchContainers(v.list, v.selectedItem, false)
if err != nil { if err != nil {
v.app.flash(flashErr, err.Error()) v.app.flash(flashErr, err.Error())
log.Error().Msgf("Error fetching containers %v", err) log.Error().Msgf("Error fetching containers %v", err)
return evt return evt
} }
if len(cc) == 1 { if len(cc) == 1 {
v.shellIn(v.selectedItem, "") v.shellIn(v.selectedItem, "")
return nil return nil
} }
p := v.GetPrimitive("picker").(*selectList) p := v.GetPrimitive("picker").(*selectList)
p.populate(cc) p.populate(cc)
p.SetSelectedFunc(func(i int, t, d string, r rune) { p.SetSelectedFunc(func(i int, t, d string, r rune) {
@ -194,14 +194,16 @@ func (v *podView) shellIn(path, co string) {
func (v *podView) extraActions(aa keyActions) { func (v *podView) extraActions(aa keyActions) {
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
aa[KeyShiftL] = newKeyAction("Logs", v.prevLogsCmd, true) aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true) aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true)
aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true) aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true)
aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true) aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true)
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true) aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true)
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true) aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true)
aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(7, true), true) aa[KeyAltC] = newKeyAction("Sort %CPU", v.sortColCmd(6, false), true)
aa[KeyAltM] = newKeyAction("Sort %MEM", v.sortColCmd(7, false), true)
aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(8, true), true)
} }
func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {

View File

@ -9,19 +9,17 @@ import (
) )
type ( type (
viewFn func(ns string, app *appView, list resource.List) resourceViewer viewFn func(ns string, app *appView, list resource.List) resourceViewer
listFn func(c resource.Connection, ns string) resource.List listFn func(c resource.Connection, ns string) resource.List
// listMxFn func(c resource.Connection, mx resource.MetricsServer, ns string) resource.List
colorerFn func(ns string, evt *resource.RowEvent) tcell.Color colorerFn func(ns string, evt *resource.RowEvent) tcell.Color
enterFn func(app *appView, ns, resource, selection string) enterFn func(app *appView, ns, resource, selection string)
decorateFn func(resource.TableData) resource.TableData decorateFn func(resource.TableData) resource.TableData
resCmd struct { resCmd struct {
title string title string
api string api string
viewFn viewFn viewFn viewFn
listFn listFn listFn listFn
// listMxFn listMxFn
enterFn enterFn enterFn enterFn
colorerFn colorerFn colorerFn colorerFn
decorateFn decorateFn decorateFn decorateFn
@ -297,9 +295,14 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
}, },
} }
rev, ok := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"}) rev, ok, err := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"})
if err != nil {
log.Error().Err(err).Msg("Checking HPA")
return cmds
}
if !ok { if !ok {
log.Warn().Msg("HPA are not supported on this cluster") log.Error().Msg("HPA are not supported on this cluster")
return cmds
} }
switch rev { switch rev {
@ -328,7 +331,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
listFn: resource.NewHorizontalPodAutoscalerList, listFn: resource.NewHorizontalPodAutoscalerList,
} }
default: default:
log.Panic().Msgf("K9s does not currently support HPA version `%s`", rev) log.Panic().Msgf("K9s unsupported HPA version. Exiting!")
} }
return cmds return cmds

View File

@ -110,6 +110,15 @@ func (v *resourceView) init(ctx context.Context, ns string) {
} }
} }
func (v *resourceView) reloadList(list resource.List, ns string) {
v.suspend()
{
v.list = list
v.list.SetNamespace(ns)
}
v.resume()
}
func (v *resourceView) updater(ctx context.Context) { func (v *resourceView) updater(ctx context.Context) {
go func(ctx context.Context) { go func(ctx context.Context) {
for { for {
@ -388,7 +397,6 @@ func (v *resourceView) refresh() {
} }
v.refreshActions() v.refreshActions()
if err := v.list.Reconcile(v.app.informer, v.path); err != nil { if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
log.Error().Err(err).Msg("Reconciliation failed") log.Error().Err(err).Msg("Reconciliation failed")
v.app.flash(flashErr, err.Error()) v.app.flash(flashErr, err.Error())
@ -443,6 +451,7 @@ func (v *resourceView) switchPage(p string) {
v.app.setHints(h.hints()) v.app.setHints(h.hints())
} }
log.Info().Msgf("Current page %#v", v.CurrentPage())
if _, ok := v.CurrentPage().Item.(*tableView); ok { if _, ok := v.CurrentPage().Item.(*tableView); ok {
v.resume() v.resume()
} }

38
internal/views/status.go Normal file
View File

@ -0,0 +1,38 @@
package views
import (
"fmt"
"github.com/derailed/tview"
)
type statusView struct {
*tview.TextView
app *appView
}
func newStatusView(app *appView) *statusView {
v := statusView{app: app, TextView: tview.NewTextView()}
{
v.SetBackgroundColor(app.styles.BgColor())
v.SetTextAlign(tview.AlignRight)
// v.SetBorderPadding(0, 0, 1, 1)
v.SetDynamicColors(true)
}
return &v
}
func (v *statusView) update(status []string) {
v.Clear()
last, bgColor := len(status)-1, v.app.styles.Style.Crumb.BgColor
for i, c := range status {
if i == last {
bgColor = v.app.styles.Style.Crumb.ActiveColor
}
fmt.Fprintf(v, "[%s:%s:b] %s [-:%s:-] ",
v.app.styles.Style.Crumb.FgColor,
bgColor, c,
v.app.styles.Style.BgColor)
}
}

45
internal/views/styles.go Normal file
View File

@ -0,0 +1,45 @@
package views
import (
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
type styles struct {
color tcell.Color
attrs tcell.AttrMask
align int
}
func stylesFor(app *appView, res string, col int) styles {
switch res {
case "pod":
return podStyles(app, col)
default:
return defaultStyles(app, col)
}
}
func podStyles(app *appView, col int) styles {
st := styles{
color: stdColor,
attrs: tcell.AttrReverse,
align: tview.AlignLeft,
}
switch col {
case 5, 6, 7, 8:
st.align = tview.AlignLeft
st.color = tcell.ColorGreen
}
return st
}
func defaultStyles(app *appView, col int) styles {
return styles{
color: tcell.ColorRed,
attrs: tcell.AttrReverse,
align: tview.AlignLeft,
}
}

View File

@ -16,9 +16,16 @@ import (
) )
const ( const (
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] " titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] "
searchFmt = "<[filter:bg:b]/%s[fg:bg:]> " searchFmt = "<[filter:bg:b]/%s[fg:bg:]> "
nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
descIndicator = "↓"
ascIndicator = "↑"
)
var (
crx = regexp.MustCompile(`\A.{0,1}CPU`)
mrx = regexp.MustCompile(`\A.{0,1}MEM`)
) )
type ( type (
@ -110,6 +117,9 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
key = tcell.Key(evt.Rune()) key = tcell.Key(evt.Rune())
if evt.Modifiers() == tcell.ModAlt {
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
}
} }
if a, ok := v.actions[key]; ok { if a, ok := v.actions[key]; ok {
@ -181,8 +191,8 @@ func (v *tableView) sortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKe
} else { } else {
v.sortCol.index, v.sortCol.asc = v.nameColIndex()+col, true v.sortCol.index, v.sortCol.asc = v.nameColIndex()+col, true
} }
v.refresh() v.refresh()
return nil return nil
} }
} }
@ -289,11 +299,11 @@ func (v *tableView) sortIndicator(index int, name string) string {
return name return name
} }
order := "↓" order := descIndicator
if v.sortCol.asc { if v.sortCol.asc {
order = "↑" order = ascIndicator
} }
return fmt.Sprintf("%s [%s::]%s[::]", name, v.app.styles.Style.Table.Header.SorterColor, order) return fmt.Sprintf("%s[%s::]%s[::]", name, v.app.styles.Style.Table.Header.SorterColor, order)
} }
func (v *tableView) doUpdate(data resource.TableData) { func (v *tableView) doUpdate(data resource.TableData) {
@ -324,7 +334,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
fg := config.AsColor(v.app.styles.Style.Table.Header.FgColor) fg := config.AsColor(v.app.styles.Style.Table.Header.FgColor)
bg := config.AsColor(v.app.styles.Style.Table.Header.BgColor) bg := config.AsColor(v.app.styles.Style.Table.Header.BgColor)
for col, h := range data.Header { for col, h := range data.Header {
v.addHeaderCell(col, h, pads, fg, bg) v.addHeaderCell(col, h, fg, bg)
} }
row++ row++
@ -340,11 +350,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
fgColor = v.colorerFn(data.Namespace, data.Rows[sk]) fgColor = v.colorerFn(data.Namespace, data.Rows[sk])
} }
for col, field := range data.Rows[sk].Fields { for col, field := range data.Rows[sk].Fields {
var age bool v.addBodyCell(data.Header[col], row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
if data.Header[col] == "AGE" {
age = true
}
v.addBodyCell(age, row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
} }
row++ row++
} }
@ -372,34 +378,43 @@ func (v *tableView) sortAllRows(rows resource.RowEvents, sortFn sortFn) (resourc
return prim, sec return prim, sec
} }
func (v *tableView) addHeaderCell(col int, name string, pads maxyPad, fg, bg tcell.Color) { func (v *tableView) addHeaderCell(col int, name string, fg, bg tcell.Color) {
c := tview.NewTableCell(v.sortIndicator(col, name)) c := tview.NewTableCell(v.sortIndicator(col, name))
{ {
c.SetExpansion(1) c.SetExpansion(1)
c.SetTextColor(fg) c.SetTextColor(fg)
if crx.MatchString(name) || mrx.MatchString(name) {
c.SetAlign(tview.AlignRight)
}
c.SetBackgroundColor(bg) c.SetBackgroundColor(bg)
} }
v.SetCell(0, col, c) v.SetCell(0, col, c)
} }
func (v *tableView) addBodyCell(age bool, row, col int, field, delta string, color tcell.Color, pads maxyPad) { func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
dField := field const colPadding = 3
if age {
if header == "AGE" {
dur, err := time.ParseDuration(field) dur, err := time.ParseDuration(field)
if err == nil { if err == nil {
dField = duration.HumanDuration(dur) field = duration.HumanDuration(dur)
} }
} }
dField += deltas(delta, field) field += deltas(delta, field)
if isASCII(field) { align := tview.AlignLeft
dField = pad(dField, pads[col]+5) if crx.MatchString(header) || mrx.MatchString(header) {
align = tview.AlignRight
} else if isASCII(field) {
field = pad(field, pads[col]+colPadding)
} }
c := tview.NewTableCell(dField) c := tview.NewTableCell(field)
{ {
c.SetExpansion(1) c.SetExpansion(1)
c.SetAlign(align)
c.SetTextColor(color) c.SetTextColor(color)
c.SetMaxWidth(pads[col] + colPadding)
} }
v.SetCell(row, col, c) v.SetCell(row, col, c)
} }

View File

@ -4,11 +4,15 @@ import (
"fmt" "fmt"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
) )
// AllNamespaces designates all namespaces.
const AllNamespaces = ""
type ( type (
// Row represents a collection of string fields. // Row represents a collection of string fields.
Row []string Row []string
@ -53,13 +57,30 @@ type Meta struct {
// NewMeta creates a new cluster resource informer // NewMeta creates a new cluster resource informer
func NewMeta(client k8s.Connection, ns string) *Meta { func NewMeta(client k8s.Connection, ns string) *Meta {
m := Meta{client: client, informers: map[string]StoreInformer{}} m := Meta{client: client, informers: map[string]StoreInformer{}}
m.init("")
nsAccess := m.client.CanIAccess("", "", "namespaces", []string{"list", "watch"})
ns, err := client.Config().CurrentNamespaceName()
// User did not lock NS. Check all ns access if not bail
if err != nil && !nsAccess {
log.Panic().Msg("Unauthorized access to list namespaces. Please specify a namespace")
}
// Namespace is locks in check if user has auth for this ns access.
if ns != AllNamespaces && !nsAccess {
if !m.client.CanIAccess("", ns, "namespaces", []string{"get", "watch"}) {
log.Panic().Msgf("Unauthorized access to namespace %q", ns)
}
m.init(ns)
} else {
m.init(AllNamespaces)
}
return &m return &m
} }
func (m *Meta) init(ns string) { func (m *Meta) init(ns string) {
po := NewPod(m.client, ns) po := NewPod(m.client, ns)
m.informers = map[string]StoreInformer{ m.informers = map[string]StoreInformer{
NodeIndex: NewNode(m.client), NodeIndex: NewNode(m.client),
PodIndex: po, PodIndex: po,
@ -67,17 +88,19 @@ func (m *Meta) init(ns string) {
} }
if m.client.HasMetrics() { if m.client.HasMetrics() {
m.informers[NodeMXIndex] = NewNodeMetrics(m.client) if m.client.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"}) {
m.informers[PodMXIndex] = NewPodMetrics(m.client, ns) m.informers[NodeMXIndex] = NewNodeMetrics(m.client)
m.informers[PodMXIndex] = NewPodMetrics(m.client, ns)
}
} }
} }
// CheckAccess checks if current user as enought RBAC fu to access watched resources. // CheckAccess checks if current user as enought RBAC fu to access watched resources.
func (m *Meta) checkAccess(ns string) error { func (m *Meta) checkAccess(ns string) error {
if !m.client.CanIAccess(ns, "nodes", "node.v1", []string{"list", "watch"}) { if !m.client.CanIAccess(ns, "nodes", "nodes", []string{"list", "watch"}) {
return fmt.Errorf("Not authorized to list/watch nodes") return fmt.Errorf("Not authorized to list/watch nodes")
} }
if !m.client.CanIAccess(ns, "pods", "pod.v1", []string{"list", "watch"}) { if !m.client.CanIAccess(ns, "pods", "pods", []string{"list", "watch"}) {
return fmt.Errorf("Not authorized to list/watch pods in namespace %s", ns) return fmt.Errorf("Not authorized to list/watch pods in namespace %s", ns)
} }

View File

@ -3,51 +3,64 @@ package watch
import ( import (
"testing" "testing"
"github.com/derailed/k9s/internal/k8s"
m "github.com/petergtz/pegomock"
"gotest.tools/assert" "gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
) )
func TestMetaList(t *testing.T) { func TestMetaList(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
cmo := NewMockConnection() cmo := NewMockConnection()
m := NewMeta(cmo, "") m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
meta := NewMeta(cmo, "")
o, err := m.List(PodIndex, "fred", metav1.ListOptions{}) o, err := meta.List(PodIndex, "fred", metav1.ListOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, len(o) == 0) assert.Assert(t, len(o) == 0)
} }
func TestMetaListNoRes(t *testing.T) { func TestMetaListNoRes(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
cmo := NewMockConnection() cmo := NewMockConnection()
m := NewMeta(cmo, "") m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
meta := NewMeta(cmo, "")
o, err := m.List("dp", "fred", metav1.ListOptions{}) o, err := meta.List("dp", "fred", metav1.ListOptions{})
assert.ErrorContains(t, err, "No informer found") assert.ErrorContains(t, err, "No informer found")
assert.Assert(t, len(o) == 0) assert.Assert(t, len(o) == 0)
} }
func TestMetaGet(t *testing.T) { func TestMetaGet(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
cmo := NewMockConnection() cmo := NewMockConnection()
m := NewMeta(cmo, "") m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
meta := NewMeta(cmo, "")
o, err := m.Get(PodIndex, "fred", metav1.GetOptions{}) o, err := meta.Get(PodIndex, "fred", metav1.GetOptions{})
assert.ErrorContains(t, err, "Pod fred not found") assert.ErrorContains(t, err, "Pod fred not found")
assert.Assert(t, o == nil) assert.Assert(t, o == nil)
} }
func TestMetaGetNoRes(t *testing.T) { func TestMetaGetNoRes(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
cmo := NewMockConnection() cmo := NewMockConnection()
m := NewMeta(cmo, "") m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
meta := NewMeta(cmo, "")
o, err := m.Get("rs", "fred", metav1.GetOptions{}) o, err := meta.Get("rs", "fred", metav1.GetOptions{})
assert.ErrorContains(t, err, "No informer found") assert.ErrorContains(t, err, "No informer found")
assert.Assert(t, o == nil) assert.Assert(t, o == nil)
} }
func TestMetaRun(t *testing.T) { func TestMetaRun(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
cmo := NewMockConnection() cmo := NewMockConnection()
m := NewMeta(cmo, "") m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
meta := NewMeta(cmo, "")
c := make(chan struct{}) c := make(chan struct{})
m.Run(c) meta.Run(c)
close(c) close(c)
} }

View File

@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
return ret0, ret1 return ret0, ret1
} }
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) { func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
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{_param0, _param1} params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()}) result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string var ret0 string
var ret1 bool var ret1 bool
var ret2 error
if len(result) != 0 { if len(result) != 0 {
if result[0] != nil { if result[0] != nil {
ret0 = result[0].(string) ret0 = result[0].(string)
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
if result[1] != nil { if result[1] != nil {
ret1 = result[1].(bool) ret1 = result[1].(bool)
} }
if result[2] != nil {
ret2 = result[2].(error)
}
} }
return ret0, ret1 return ret0, ret1, ret2
} }
func (mock *MockConnection) SupportsResource(_param0 string) bool { func (mock *MockConnection) SupportsResource(_param0 string) bool {

View File

@ -64,7 +64,6 @@ func newNodeMetricsInformer(client k8s.Connection, sync time.Duration, idxs cach
if err != nil { if err != nil {
return nil, err return nil, err
} }
l, err := c.MetricsV1beta1().NodeMetricses().List(opts) l, err := c.MetricsV1beta1().NodeMetricses().List(opts)
if err == nil { if err == nil {
pw.update(l, false) pw.update(l, false)
@ -113,7 +112,6 @@ func (n *nodeMxWatcher) Run() {
if err != nil { if err != nil {
return return
} }
list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{}) list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
if err != nil { if err != nil {
log.Error().Err(err).Msg("Fetch node metrics") log.Error().Err(err).Msg("Fetch node metrics")
@ -142,7 +140,6 @@ func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) {
fqn := MetaFQN(list.Items[i].ObjectMeta) fqn := MetaFQN(list.Items[i].ObjectMeta)
fqns[fqn] = &list.Items[i] fqns[fqn] = &list.Items[i]
} }
for k, v := range n.cache { for k, v := range n.cache {
if _, ok := fqns[k]; !ok { if _, ok := fqns[k]; !ok {
if notify { if notify {
@ -154,7 +151,6 @@ func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) {
delete(n.cache, k) delete(n.cache, k)
} }
} }
for k, v := range fqns { for k, v := range fqns {
kind := watch.Added kind := watch.Added
if v1, ok := n.cache[k]; ok { if v1, ok := n.cache[k]; ok {
@ -163,7 +159,6 @@ func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) {
} }
kind = watch.Modified kind = watch.Modified
} }
if notify { if notify {
n.eventChan <- watch.Event{ n.eventChan <- watch.Event{
Type: kind, Type: kind,

View File

@ -1,7 +1,6 @@
package watch package watch
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
@ -75,11 +74,6 @@ func newPodMetricsInformer(client k8s.Connection, ns string, sync time.Duration,
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !client.HasMetrics() {
return nil, errors.New("metrics-server not supported")
}
l, err := c.MetricsV1beta1().PodMetricses(ns).List(opts) l, err := c.MetricsV1beta1().PodMetricses(ns).List(opts)
if err == nil { if err == nil {
pw.update(l, false) pw.update(l, false)

View File

@ -2,14 +2,12 @@ package main
import ( import (
"os" "os"
"syscall"
"github.com/derailed/k9s/cmd" "github.com/derailed/k9s/cmd"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/klog"
) )
func init() { func init() {
@ -18,9 +16,6 @@ func init() {
mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY
if file, err := os.OpenFile(config.K9sLogs, mod, config.DefaultFileMod); err == nil { if file, err := os.OpenFile(config.K9sLogs, mod, config.DefaultFileMod); err == nil {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
// Klogs (of course) want to print stuff to the screen ;(
klog.SetOutput(file)
syscall.Dup2(int(file.Fd()), 2)
} else { } else {
panic(err) panic(err)
} }