add ns checks, percentage + bugz fixes
parent
0cb1a9766f
commit
21ed022434
|
|
@ -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
|
||||
|
|
@ -12,7 +12,7 @@ for changes and offers subsequent commands to interact with observed resources.
|
|||
[](https://goreportcard.com/report/github.com/derailed/k9s)
|
||||
[](https://travis-ci.com/derailed/k9s)
|
||||
[](https://github.com/derailed/k9s/releases)
|
||||
[](https://snapcraft.io/k9s)
|
||||
<!-- [](https://snapcraft.io/k9s) -->
|
||||
[](https://github.com/mum4k/termdash/blob/master/LICENSE)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
3
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ func newTableHeader() *TableHeader {
|
|||
return &TableHeader{
|
||||
FgColor: "white",
|
||||
BgColor: "black",
|
||||
SorterColor: "orange",
|
||||
SorterColor: "aqua",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
return resp.Status.Allowed
|
||||
allow = true
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 "⬇︎"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.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 = newDetailsView(parent.appView(), parent.backFn())
|
||||
{
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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
|
||||
colorerFn func(ns string, evt *resource.RowEvent) tcell.Color
|
||||
enterFn func(app *appView, ns, resource, selection string)
|
||||
decorateFn func(resource.TableData) resource.TableData
|
||||
|
|
@ -21,7 +20,6 @@ type (
|
|||
api string
|
||||
viewFn viewFn
|
||||
listFn listFn
|
||||
// listMxFn listMxFn
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,13 @@ 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:-] "
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue