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

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
import (
"flag"
"fmt"
"runtime/debug"
@ -12,6 +13,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/klog"
)
const (
@ -41,6 +43,13 @@ func init() {
rootCmd.AddCommand(versionCmd(), infoCmd())
initK9sFlags()
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

3
go.mod
View File

@ -4,7 +4,9 @@ go 1.12
replace (
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/apiserver => k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe
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/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/hashicorp/golang-lru v0.5.1 // 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/mattn/go-runewidth v0.0.4
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=
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/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/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/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-20190426133039-accf7b6d6716 h1:gByi/idNjfDDk+lWNRqWk2uE1/KAsJtYXRMEc2M1a1k=
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
}
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) {
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
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 ret1 bool
var ret2 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
if result[1] != nil {
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 {

View File

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

View File

@ -3,6 +3,7 @@ package k8s
import (
"fmt"
"strings"
"sync"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -59,7 +60,7 @@ type (
SupportsResource(group string) bool
ValidNamespaces() ([]v1.Namespace, 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)
FetchNodes() (*v1.NodeList, error)
CurrentNamespaceName() (string, error)
@ -75,6 +76,7 @@ type (
mxsClient *versioned.Clientset
useMetricServer bool
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 err error
var allow bool
for _, v := range verbs {
sar.Spec.ResourceAttributes.Verb = v
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")
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
}
return resp.Status.Allowed
log.Debug().Msgf("GRANT ACCESS:%t", allow)
return allow
}
// 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.
func (a *APIClient) NSDialOrDie() dynamic.NamespaceableResourceInterface {
a.mx.Lock()
defer a.mx.Unlock()
if a.nsClient != nil {
return a.nsClient
}
a.nsClient = a.DynDialOrDie().Resource(schema.GroupVersionResource{
Group: "apiextensions.k8s.io",
Version: "v1beta1",
Resource: "customresourcedefinitions",
})
return a.nsClient
}
// MXDial returns a handle to the metrics server.
func (a *APIClient) MXDial() (*versioned.Clientset, error) {
a.mx.Lock()
defer a.mx.Unlock()
if a.mxsClient != nil {
return a.mxsClient, nil
}
var err error
if a.mxsClient, err = versioned.NewForConfig(a.RestConfigOrDie()); err != nil {
a.log.Debug().Err(err)
}
return a.mxsClient, err
}
@ -299,27 +313,18 @@ func (a *APIClient) supportsMxServer() bool {
}
// 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()
if err != nil {
log.Error().Err(err).Msg("Unable to dial api groups")
return "", false
return "", false, err
}
for _, grp := range apiGroups.Groups {
if grp.Name != group {
continue
}
return grp.PreferredVersion.Version, true
// for _, version := range grp.Versions {
// for _, supportedVersion := range versions {
// if version.Version == supportedVersion {
// return supportedVersion, true
// }
// }
// }
return grp.PreferredVersion.Version, true, nil
}
return "", false
return "", false, nil
}

View File

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

View File

@ -81,7 +81,7 @@ func join(a []string, sep string) string {
// AsPerc prints a number as a percentage.
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.
@ -168,12 +168,12 @@ func mapToStr(m map[string]string) (s string) {
// ToMillicore shows cpu reading for human.
func ToMillicore(v int64) string {
return strconv.Itoa(int(v)) + "m"
return strconv.Itoa(int(v))
}
// ToMi shows mem reading for human.
func ToMi(v float64) string {
return strconv.Itoa(int(v)) + "Mi"
return strconv.Itoa(int(v))
}
func boolPtrToStr(b *bool) string {

View File

@ -142,9 +142,9 @@ func TestToMillicore(t *testing.T) {
v int64
e string
}{
{0, "0m"},
{2, "2m"},
{1000, "1000m"},
{0, "0"},
{2, "2"},
{1000, "1000"},
}
for _, u := range uu {
@ -157,9 +157,9 @@ func TestToMi(t *testing.T) {
v float64
e string
}{
{0, "0Mi"},
{2, "2Mi"},
{1000, "1000Mi"},
{0, "0"},
{2, "2"},
{1000, "1000"},
}
for _, u := range uu {
@ -172,10 +172,10 @@ func TestAsPerc(t *testing.T) {
v float64
e string
}{
{0, "0%"},
{10.5, "10%"},
{10, "10%"},
{0.05, "0%"},
{0, "0"},
{10.5, "10"},
{10, "10"},
{0.05, "0"},
}
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(),
})
if err != nil {
log.Debug().Msgf(">>>>>> DOH! %#v", err)
return nil, err
}

View File

@ -288,14 +288,15 @@ func (mock *MockClusterMeta) ServerVersion() (*version.Info, error) {
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 {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
}
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 ret1 bool
var ret2 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
@ -303,8 +304,11 @@ func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (stri
if result[1] != nil {
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 {

View File

@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
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 {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
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 ret1 bool
var ret2 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
if result[1] != nil {
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 {

View File

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

View File

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

View File

@ -62,21 +62,6 @@ func (r *Namespace) Marshal(path string) (string, error) {
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.
func (*Namespace) Header(ns string) Row {
return Row{"NAME", "STATUS", "AGE"}

View File

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

View File

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

View File

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

View File

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

View File

@ -10,14 +10,16 @@ type containerView struct {
*resourceView
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.path = &path
v.extraActionsFn = v.extraActions
v.current = app.content.GetPrimitive("main").(igniter)
v.exitFn = exitFn
}
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
v.switchPage("co")
@ -97,13 +99,15 @@ func (v *containerView) shellIn(path, co string) {
func (v *containerView) extraActions(aa keyActions) {
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[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false)
aa[KeyP] = newKeyAction("Previous", v.backCmd, false)
aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false)
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true)
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true)
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, 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 {
@ -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 {
v.app.inject(v.current)
// v.app.inject(v.current)
v.exitFn()
return nil
}

View File

@ -48,7 +48,27 @@ func (v *flashView) setMessage(level flashLevel, msg ...string) {
if v.cancel != nil {
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()
if width <= 15 {
width = 100
@ -56,23 +76,6 @@ func (v *flashView) setMessage(level flashLevel, msg ...string) {
m := strings.Join(msg, " ")
v.SetTextColor(flashColor(level))
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 {

View File

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

View File

@ -6,45 +6,154 @@ import (
"strings"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
type logView struct {
*detailsView
*tview.Flex
app *appView
logs *detailsView
status *statusView
parent masterView
ansiWriter io.Writer
autoScroll bool
actions keyActions
}
func newLogView(title string, parent loggable) *logView {
v := logView{detailsView: newDetailsView(parent.appView(), parent.backFn())}
func newLogView(title string, parent masterView) *logView {
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.setCategory("Logs")
v.SetDynamicColors(true)
v.SetWrap(true)
v.setTitle(parent.getSelection())
v.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize)
v.ansiWriter = tview.ANSIWriter(v)
v.logs.SetBorder(false)
v.logs.setCategory("Logs")
v.logs.SetDynamicColors(true)
v.logs.SetWrap(true)
v.logs.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize)
}
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
}
func (l *logView) logLine(line string) {
fmt.Fprintln(l.ansiWriter, tview.Escape(line))
// Hints show action hints
func (v *logView) hints() hints {
return v.actions.toHints()
}
func (l *logView) log(lines fmt.Stringer) {
l.Clear()
fmt.Fprintln(l.ansiWriter, lines.String())
func (v *logView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key()
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) {
if index > 0 {
l.logLine(strings.Join(buff[:index], "\n"))
if scroll {
l.app.QueueUpdate(func() {
l.ScrollToEnd()
})
}
func (v *logView) logLine(line string) {
fmt.Fprintln(v.ansiWriter, tview.Escape(line))
}
func (v *logView) log(lines fmt.Stringer) {
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 (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/derailed/k9s/internal/resource"
@ -20,6 +20,11 @@ const (
flushTimeout = 200 * time.Millisecond
)
type masterView interface {
backFn() actionHandler
appView() *appView
}
type logsView struct {
*tview.Pages
@ -28,7 +33,6 @@ type logsView struct {
containers []string
actions keyActions
cancelFunc context.CancelFunc
autoScroll bool
showPrevious bool
}
@ -37,19 +41,8 @@ func newLogsView(pview string, parent loggable) *logsView {
Pages: tview.NewPages(),
parent: parent,
parentView: pview,
autoScroll: true,
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
}
@ -63,35 +56,6 @@ func (v *logsView) reload(co string, parent loggable, view string, prevLogs bool
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.
func (v *logsView) setActions(aa keyActions) {
v.actions = aa
@ -99,25 +63,24 @@ func (v *logsView) setActions(aa keyActions) {
// Hints show action hints
func (v *logsView) hints() hints {
if len(v.containers) > 1 {
for i, c := range v.containers {
v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil, true)
}
}
return v.actions.toHints()
l := v.CurrentPage().Item.(*logView)
return l.actions.toHints()
}
func (v *logsView) addContainer(n string) {
v.containers = append(v.containers, n)
l := newLogView(n, v.parent)
{
l.SetInputCapture(v.keyboard)
l.backFn = v.backCmd
}
l := newLogView(n, v)
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() {
for i, c := range v.containers {
v.RemovePage(c)
@ -130,7 +93,6 @@ func (v *logsView) stop() {
if v.cancelFunc == nil {
return
}
v.cancelFunc()
log.Debug().Msgf("Canceling logs...")
v.cancelFunc = nil
@ -140,7 +102,6 @@ func (v *logsView) load(i int) {
if i < 0 || i > len(v.containers)-1 {
return
}
v.SwitchToPage(v.containers[i])
if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
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)
l := v.CurrentPage().Item.(*logView)
l.Clear()
l.setTitle(path + ":" + co)
l.logs.Clear()
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)
go func(l *logView) {
@ -166,7 +131,7 @@ func (v *logsView) doLoad(path, co string) error {
select {
case line, ok := <-c:
if !ok {
l.flush(index, buff, v.autoScroll)
l.flush(index, buff)
index = 0
return
}
@ -175,11 +140,11 @@ func (v *logsView) doLoad(path, co string) error {
index++
continue
}
l.flush(index, buff, v.autoScroll)
l.flush(index, buff)
index = 0
buff[index] = line
case <-time.After(flushTimeout):
l.flush(index, buff, v.autoScroll)
l.flush(index, buff)
index = 0
}
}
@ -204,67 +169,9 @@ func (v *logsView) doLoad(path, co string) error {
// ----------------------------------------------------------------------------
// 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 {
v.stop()
v.parent.switchPage(v.parentView)
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
)
// 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
const (
KeyA tcell.Key = iota + 97
@ -351,4 +381,32 @@ func initKeys() {
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
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
}
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool) {
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
}
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 ret1 bool
var ret2 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
@ -303,8 +304,11 @@ func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (stri
if result[1] != nil {
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 {

View File

@ -1,6 +1,7 @@
package views
import (
"context"
"fmt"
"strings"
@ -17,6 +18,8 @@ const containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
type podView struct {
*resourceView
cancel context.CancelFunc
}
type loggable interface {
@ -33,7 +36,6 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer {
v.extraActionsFn = v.extraActions
v.enterFn = v.listContainers
}
picker := newSelectList(&v)
{
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("logs", newLogsView(list.GetName(), &v), true, false)
v.switchPage("po")
@ -52,14 +53,12 @@ func (v *podView) listContainers(app *appView, _, res, sel string) {
if !v.rowSelected() {
return
}
po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{})
if err != nil {
log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel)
app.flash(flashErr, err.Error())
return
}
pod := po.(*v1.Pod)
mx := k8s.NewMetricsServer(app.conn())
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(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
title := fmt.Sprintf(fmat, "Containers", sel)
app.inject(newContainerView(
title,
app,
list,
namespacedName(pod.Namespace, pod.Name),
))
v.suspend()
cv := newContainerView(title, app, list, namespacedName(pod.Namespace, pod.Name), v.exitFn)
v.AddPage("containers", cv, true, true)
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...
@ -116,19 +123,16 @@ func (v *podView) viewLogs(prev bool) bool {
if !v.rowSelected() {
return false
}
cc, err := fetchContainers(v.list, v.selectedItem, true)
if err != nil {
v.app.flash(flashErr, err.Error())
log.Error().Err(err)
return false
}
if len(cc) == 1 {
v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, prev)
return true
}
picker := v.GetPrimitive("picker").(*selectList)
picker.populate(cc)
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) {
l := v.GetPrimitive("logs").(*logsView)
l.reload(co, parent, view, prev)
v.switchPage("logs")
}
@ -150,19 +153,16 @@ func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() {
return evt
}
cc, err := fetchContainers(v.list, v.selectedItem, false)
if err != nil {
v.app.flash(flashErr, err.Error())
log.Error().Msgf("Error fetching containers %v", err)
return evt
}
if len(cc) == 1 {
v.shellIn(v.selectedItem, "")
return nil
}
p := v.GetPrimitive("picker").(*selectList)
p.populate(cc)
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) {
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[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true)
aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true)
aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true)
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, 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 {

View File

@ -9,19 +9,17 @@ import (
)
type (
viewFn func(ns string, app *appView, list resource.List) resourceViewer
listFn func(c resource.Connection, ns string) resource.List
// listMxFn func(c resource.Connection, mx resource.MetricsServer, ns string) resource.List
viewFn func(ns string, app *appView, list resource.List) resourceViewer
listFn func(c resource.Connection, ns string) resource.List
colorerFn func(ns string, evt *resource.RowEvent) tcell.Color
enterFn func(app *appView, ns, resource, selection string)
decorateFn func(resource.TableData) resource.TableData
resCmd struct {
title string
api string
viewFn viewFn
listFn listFn
// listMxFn listMxFn
title string
api string
viewFn viewFn
listFn listFn
enterFn enterFn
colorerFn colorerFn
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 {
log.Warn().Msg("HPA are not supported on this cluster")
log.Error().Msg("HPA are not supported on this cluster")
return cmds
}
switch rev {
@ -328,7 +331,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
listFn: resource.NewHorizontalPodAutoscalerList,
}
default:
log.Panic().Msgf("K9s does not currently support HPA version `%s`", rev)
log.Panic().Msgf("K9s unsupported HPA version. Exiting!")
}
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) {
go func(ctx context.Context) {
for {
@ -388,7 +397,6 @@ func (v *resourceView) refresh() {
}
v.refreshActions()
if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
log.Error().Err(err).Msg("Reconciliation failed")
v.app.flash(flashErr, err.Error())
@ -443,6 +451,7 @@ func (v *resourceView) switchPage(p string) {
v.app.setHints(h.hints())
}
log.Info().Msgf("Current page %#v", v.CurrentPage())
if _, ok := v.CurrentPage().Item.(*tableView); ok {
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 (
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[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:-] "
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[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:-] "
descIndicator = "↓"
ascIndicator = "↑"
)
var (
crx = regexp.MustCompile(`\A.{0,1}CPU`)
mrx = regexp.MustCompile(`\A.{0,1}MEM`)
)
type (
@ -110,6 +117,9 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
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 {
@ -181,8 +191,8 @@ func (v *tableView) sortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKe
} else {
v.sortCol.index, v.sortCol.asc = v.nameColIndex()+col, true
}
v.refresh()
return nil
}
}
@ -289,11 +299,11 @@ func (v *tableView) sortIndicator(index int, name string) string {
return name
}
order := "↓"
order := descIndicator
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) {
@ -324,7 +334,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
fg := config.AsColor(v.app.styles.Style.Table.Header.FgColor)
bg := config.AsColor(v.app.styles.Style.Table.Header.BgColor)
for col, h := range data.Header {
v.addHeaderCell(col, h, pads, fg, bg)
v.addHeaderCell(col, h, fg, bg)
}
row++
@ -340,11 +350,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
fgColor = v.colorerFn(data.Namespace, data.Rows[sk])
}
for col, field := range data.Rows[sk].Fields {
var age bool
if data.Header[col] == "AGE" {
age = true
}
v.addBodyCell(age, row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
v.addBodyCell(data.Header[col], row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
}
row++
}
@ -372,34 +378,43 @@ func (v *tableView) sortAllRows(rows resource.RowEvents, sortFn sortFn) (resourc
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.SetExpansion(1)
c.SetTextColor(fg)
if crx.MatchString(name) || mrx.MatchString(name) {
c.SetAlign(tview.AlignRight)
}
c.SetBackgroundColor(bg)
}
v.SetCell(0, col, c)
}
func (v *tableView) addBodyCell(age bool, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
dField := field
if age {
func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
const colPadding = 3
if header == "AGE" {
dur, err := time.ParseDuration(field)
if err == nil {
dField = duration.HumanDuration(dur)
field = duration.HumanDuration(dur)
}
}
dField += deltas(delta, field)
if isASCII(field) {
dField = pad(dField, pads[col]+5)
field += deltas(delta, field)
align := tview.AlignLeft
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.SetAlign(align)
c.SetTextColor(color)
c.SetMaxWidth(pads[col] + colPadding)
}
v.SetCell(row, col, c)
}

View File

@ -4,11 +4,15 @@ import (
"fmt"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
)
// AllNamespaces designates all namespaces.
const AllNamespaces = ""
type (
// Row represents a collection of string fields.
Row []string
@ -53,13 +57,30 @@ type Meta struct {
// NewMeta creates a new cluster resource informer
func NewMeta(client k8s.Connection, ns string) *Meta {
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
}
func (m *Meta) init(ns string) {
po := NewPod(m.client, ns)
m.informers = map[string]StoreInformer{
NodeIndex: NewNode(m.client),
PodIndex: po,
@ -67,17 +88,19 @@ func (m *Meta) init(ns string) {
}
if m.client.HasMetrics() {
m.informers[NodeMXIndex] = NewNodeMetrics(m.client)
m.informers[PodMXIndex] = NewPodMetrics(m.client, ns)
if m.client.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"}) {
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.
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")
}
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)
}

View File

@ -3,51 +3,64 @@ package watch
import (
"testing"
"github.com/derailed/k9s/internal/k8s"
m "github.com/petergtz/pegomock"
"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
func TestMetaList(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
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.Assert(t, len(o) == 0)
}
func TestMetaListNoRes(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
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.Assert(t, len(o) == 0)
}
func TestMetaGet(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
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.Assert(t, o == nil)
}
func TestMetaGetNoRes(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
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.Assert(t, o == nil)
}
func TestMetaRun(t *testing.T) {
f := new(genericclioptions.ConfigFlags)
cmo := NewMockConnection()
m := NewMeta(cmo, "")
m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
meta := NewMeta(cmo, "")
c := make(chan struct{})
m.Run(c)
meta.Run(c)
close(c)
}

View File

@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
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 {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
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 ret1 bool
var ret2 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
if result[1] != nil {
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 {

View File

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

View File

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

View File

@ -2,14 +2,12 @@ package main
import (
"os"
"syscall"
"github.com/derailed/k9s/cmd"
"github.com/derailed/k9s/internal/config"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/klog"
)
func init() {
@ -18,9 +16,6 @@ func init() {
mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY
if file, err := os.OpenFile(config.K9sLogs, mod, config.DefaultFileMod); err == nil {
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 {
panic(err)
}