add ns checks, percentage + bugz fixes
parent
0cb1a9766f
commit
21ed022434
|
|
@ -9,17 +9,17 @@ builds:
|
||||||
- env:
|
- env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
# - linux
|
- linux
|
||||||
# - darwin
|
- darwin
|
||||||
- windows
|
- windows
|
||||||
goarch:
|
goarch:
|
||||||
# - 386
|
- 386
|
||||||
- amd64
|
- amd64
|
||||||
# - arm
|
- arm
|
||||||
# - arm64
|
- arm64
|
||||||
# goarm:
|
goarm:
|
||||||
# - 6
|
- 6
|
||||||
# - 7
|
- 7
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/derailed/k9s/cmd.version={{.Version}} -X github.com/derailed/k9s/cmd.commit={{.Commit}} -X github.com/derailed/k9s/cmd.date={{.Date}}
|
- -s -w -X github.com/derailed/k9s/cmd.version={{.Version}} -X github.com/derailed/k9s/cmd.commit={{.Commit}} -X github.com/derailed/k9s/cmd.date={{.Date}}
|
||||||
archive:
|
archive:
|
||||||
|
|
@ -59,33 +59,34 @@ brew:
|
||||||
system "k9s version"
|
system "k9s version"
|
||||||
|
|
||||||
# Snapcraft
|
# Snapcraft
|
||||||
# snapcraft:
|
snapcraft:
|
||||||
# name: k9s
|
name: k9s
|
||||||
# summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
||||||
# description: |
|
description: |
|
||||||
# K9s is a CLI to view and manage your Kubernetes clusters.
|
K9s is a CLI to view and manage your Kubernetes clusters.
|
||||||
# By leveraging a terminal UI, you can easily traverse Kubernetes resources
|
By leveraging a terminal UI, you can easily traverse Kubernetes resources
|
||||||
# and view the state of you clusters in a single powerful session.
|
and view the state of you clusters in a single powerful session.
|
||||||
# name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
# publish: false
|
# publish: false
|
||||||
# # publish: true
|
publish: true
|
||||||
# replacements:
|
replacements:
|
||||||
# amd64: 64-bit
|
amd64: 64-bit
|
||||||
# 386: 32-bit
|
386: 32-bit
|
||||||
# darwin: macOS
|
darwin: macOS
|
||||||
# linux: Tux
|
linux: Tux
|
||||||
# bit: Arm
|
bit: Arm
|
||||||
# bitv6: Arm6
|
bitv6: Arm6
|
||||||
# bitv7: Arm7
|
bitv7: Arm7
|
||||||
# grade: devel
|
grade: devel
|
||||||
# confinement: devmode
|
confinement: devmode
|
||||||
# # grade: stable
|
# grade: stable
|
||||||
# # confinement: strict
|
# confinement: strict
|
||||||
# apps:
|
apps:
|
||||||
# k9s:
|
k9s:
|
||||||
# plugs: ["home", "network"]
|
# plugs: ["home", "network"]
|
||||||
# # plugs: ["home", "network", "personal-files"]
|
plugs: ["home", "network", "kube-config"]
|
||||||
# plugs:
|
plugs:
|
||||||
# personal-files:
|
kube-config:
|
||||||
# read:
|
interface: personal-files
|
||||||
# - $HOME/.kube
|
read:
|
||||||
|
- $HOME/.kube
|
||||||
|
|
@ -12,7 +12,7 @@ for changes and offers subsequent commands to interact with observed resources.
|
||||||
[](https://goreportcard.com/report/github.com/derailed/k9s)
|
[](https://goreportcard.com/report/github.com/derailed/k9s)
|
||||||
[](https://travis-ci.com/derailed/k9s)
|
[](https://travis-ci.com/derailed/k9s)
|
||||||
[](https://github.com/derailed/k9s/releases)
|
[](https://github.com/derailed/k9s/releases)
|
||||||
[](https://snapcraft.io/k9s)
|
<!-- [](https://snapcraft.io/k9s) -->
|
||||||
[](https://github.com/mum4k/termdash/blob/master/LICENSE)
|
[](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
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/klog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -41,6 +43,13 @@ func init() {
|
||||||
rootCmd.AddCommand(versionCmd(), infoCmd())
|
rootCmd.AddCommand(versionCmd(), infoCmd())
|
||||||
initK9sFlags()
|
initK9sFlags()
|
||||||
initK8sFlags()
|
initK8sFlags()
|
||||||
|
|
||||||
|
// Klogs (of course) want to print stuff to the screen ;(
|
||||||
|
klog.InitFlags(nil)
|
||||||
|
flag.Set("log_file", config.K9sLogs)
|
||||||
|
flag.Set("stderrthreshold", "fatal")
|
||||||
|
flag.Set("alsologtostderr", "false")
|
||||||
|
flag.Set("logtostderr", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute root command
|
// Execute root command
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -4,7 +4,9 @@ go 1.12
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20190222213804-5cb15d344471
|
k8s.io/api => k8s.io/api v0.0.0-20190222213804-5cb15d344471
|
||||||
|
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f
|
||||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
|
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
|
||||||
|
k8s.io/apiserver => k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe
|
||||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190325194458-f2b4781c3ae1
|
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190325194458-f2b4781c3ae1
|
||||||
k8s.io/client-go => k8s.io/client-go v10.0.0+incompatible
|
k8s.io/client-go => k8s.io/client-go v10.0.0+incompatible
|
||||||
k8s.io/metrics => k8s.io/metrics v0.0.0-20190325194013-29123f6a4aa6
|
k8s.io/metrics => k8s.io/metrics v0.0.0-20190325194013-29123f6a4aa6
|
||||||
|
|
@ -26,6 +28,7 @@ require (
|
||||||
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||||
github.com/imdario/mergo v0.3.7 // indirect
|
github.com/imdario/mergo v0.3.7 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.6 // indirect
|
github.com/json-iterator/go v1.1.6 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.4
|
github.com/mattn/go-runewidth v0.0.4
|
||||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||||
|
|
|
||||||
4
go.sum
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=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE=
|
k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE=
|
||||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
|
k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
|
||||||
|
k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f h1:+GpMltIq6SUOswgSQ3HcxgldikyBCreeRDkCYOzwfGk=
|
||||||
|
k8s.io/apiextensions-apiserver v0.0.0-20190325193600-475668423e9f/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
|
||||||
k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0 h1:blst2tV97kE1/Mxaxx3zzh6zUGpxCbGNq0CdFf9/N8s=
|
k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0 h1:blst2tV97kE1/Mxaxx3zzh6zUGpxCbGNq0CdFf9/N8s=
|
||||||
k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0/go.mod h1:IPM+7P9C3mY4uik+2wHMNbydKfSZpl9Hnu0Ze0447Wg=
|
k8s.io/apiextensions-apiserver v0.0.0-20190426053235-842c4571cde0/go.mod h1:IPM+7P9C3mY4uik+2wHMNbydKfSZpl9Hnu0Ze0447Wg=
|
||||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg=
|
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg=
|
||||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||||
|
k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe h1:zD63Eo0qbcR9JzZ90yQsFMzXYSbfsCa5ICB2D2nX1tg=
|
||||||
|
k8s.io/apiserver v0.0.0-20190319190228-a4358799e4fe/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w=
|
||||||
k8s.io/apiserver v0.0.0-20190426012941-33871ad74f4b/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80=
|
k8s.io/apiserver v0.0.0-20190426012941-33871ad74f4b/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80=
|
||||||
k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716 h1:gByi/idNjfDDk+lWNRqWk2uE1/KAsJtYXRMEc2M1a1k=
|
k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716 h1:gByi/idNjfDDk+lWNRqWk2uE1/KAsJtYXRMEc2M1a1k=
|
||||||
k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80=
|
k8s.io/apiserver v0.0.0-20190426133039-accf7b6d6716/go.mod h1:omlj40TPI/OV4YFwPP09JuOkEkKbpS5bNE2T2sPeY80=
|
||||||
|
|
|
||||||
|
|
@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) {
|
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
|
||||||
if mock == nil {
|
if mock == nil {
|
||||||
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
||||||
}
|
}
|
||||||
params := []pegomock.Param{_param0, _param1}
|
params := []pegomock.Param{_param0, _param1}
|
||||||
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
|
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
var ret0 string
|
var ret0 string
|
||||||
var ret1 bool
|
var ret1 bool
|
||||||
|
var ret2 error
|
||||||
if len(result) != 0 {
|
if len(result) != 0 {
|
||||||
if result[0] != nil {
|
if result[0] != nil {
|
||||||
ret0 = result[0].(string)
|
ret0 = result[0].(string)
|
||||||
|
|
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
|
||||||
if result[1] != nil {
|
if result[1] != nil {
|
||||||
ret1 = result[1].(bool)
|
ret1 = result[1].(bool)
|
||||||
}
|
}
|
||||||
|
if result[2] != nil {
|
||||||
|
ret2 = result[2].(error)
|
||||||
}
|
}
|
||||||
return ret0, ret1
|
}
|
||||||
|
return ret0, ret1, ret2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockConnection) SupportsResource(_param0 string) bool {
|
func (mock *MockConnection) SupportsResource(_param0 string) bool {
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ func newTableHeader() *TableHeader {
|
||||||
return &TableHeader{
|
return &TableHeader{
|
||||||
FgColor: "white",
|
FgColor: "white",
|
||||||
BgColor: "black",
|
BgColor: "black",
|
||||||
SorterColor: "orange",
|
SorterColor: "aqua",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package k8s
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -59,7 +60,7 @@ type (
|
||||||
SupportsResource(group string) bool
|
SupportsResource(group string) bool
|
||||||
ValidNamespaces() ([]v1.Namespace, error)
|
ValidNamespaces() ([]v1.Namespace, error)
|
||||||
NodePods(node string) (*v1.PodList, error)
|
NodePods(node string) (*v1.PodList, error)
|
||||||
SupportsRes(grp string, versions []string) (string, bool)
|
SupportsRes(grp string, versions []string) (string, bool, error)
|
||||||
ServerVersion() (*version.Info, error)
|
ServerVersion() (*version.Info, error)
|
||||||
FetchNodes() (*v1.NodeList, error)
|
FetchNodes() (*v1.NodeList, error)
|
||||||
CurrentNamespaceName() (string, error)
|
CurrentNamespaceName() (string, error)
|
||||||
|
|
@ -75,6 +76,7 @@ type (
|
||||||
mxsClient *versioned.Clientset
|
mxsClient *versioned.Clientset
|
||||||
useMetricServer bool
|
useMetricServer bool
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
|
mx sync.Mutex
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -104,6 +106,7 @@ func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) bool {
|
||||||
|
|
||||||
var resp *authorizationv1.SelfSubjectAccessReview
|
var resp *authorizationv1.SelfSubjectAccessReview
|
||||||
var err error
|
var err error
|
||||||
|
var allow bool
|
||||||
for _, v := range verbs {
|
for _, v := range verbs {
|
||||||
sar.Spec.ResourceAttributes.Verb = v
|
sar.Spec.ResourceAttributes.Verb = v
|
||||||
resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
|
resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
|
||||||
|
|
@ -111,9 +114,14 @@ func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) bool {
|
||||||
log.Warn().Err(err).Msgf("CanIAccess")
|
log.Warn().Err(err).Msgf("CanIAccess")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
log.Debug().Msgf("CHECKING ACCESS for %s/%s/ in NS %q verb: %s -> %t, %s", resURL, name, ns, v, resp.Status.Allowed, resp.Status.Reason)
|
||||||
|
if !resp.Status.Allowed {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
allow = true
|
||||||
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.
|
// CurrentNamespaceName return namespace name set via either cli arg or cluster config.
|
||||||
|
|
@ -230,28 +238,34 @@ func (a *APIClient) DynDialOrDie() dynamic.Interface {
|
||||||
|
|
||||||
// NSDialOrDie returns a handle to a namespaced resource.
|
// NSDialOrDie returns a handle to a namespaced resource.
|
||||||
func (a *APIClient) NSDialOrDie() dynamic.NamespaceableResourceInterface {
|
func (a *APIClient) NSDialOrDie() dynamic.NamespaceableResourceInterface {
|
||||||
|
a.mx.Lock()
|
||||||
|
defer a.mx.Unlock()
|
||||||
|
|
||||||
if a.nsClient != nil {
|
if a.nsClient != nil {
|
||||||
return a.nsClient
|
return a.nsClient
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nsClient = a.DynDialOrDie().Resource(schema.GroupVersionResource{
|
a.nsClient = a.DynDialOrDie().Resource(schema.GroupVersionResource{
|
||||||
Group: "apiextensions.k8s.io",
|
Group: "apiextensions.k8s.io",
|
||||||
Version: "v1beta1",
|
Version: "v1beta1",
|
||||||
Resource: "customresourcedefinitions",
|
Resource: "customresourcedefinitions",
|
||||||
})
|
})
|
||||||
|
|
||||||
return a.nsClient
|
return a.nsClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// MXDial returns a handle to the metrics server.
|
// MXDial returns a handle to the metrics server.
|
||||||
func (a *APIClient) MXDial() (*versioned.Clientset, error) {
|
func (a *APIClient) MXDial() (*versioned.Clientset, error) {
|
||||||
|
a.mx.Lock()
|
||||||
|
defer a.mx.Unlock()
|
||||||
|
|
||||||
if a.mxsClient != nil {
|
if a.mxsClient != nil {
|
||||||
return a.mxsClient, nil
|
return a.mxsClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if a.mxsClient, err = versioned.NewForConfig(a.RestConfigOrDie()); err != nil {
|
if a.mxsClient, err = versioned.NewForConfig(a.RestConfigOrDie()); err != nil {
|
||||||
a.log.Debug().Err(err)
|
a.log.Debug().Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.mxsClient, err
|
return a.mxsClient, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,27 +313,18 @@ func (a *APIClient) supportsMxServer() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportsRes checks latest supported version.
|
// SupportsRes checks latest supported version.
|
||||||
func (a *APIClient) SupportsRes(group string, versions []string) (string, bool) {
|
func (a *APIClient) SupportsRes(group string, versions []string) (string, bool, error) {
|
||||||
apiGroups, err := a.DialOrDie().Discovery().ServerGroups()
|
apiGroups, err := a.DialOrDie().Discovery().ServerGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Unable to dial api groups")
|
return "", false, err
|
||||||
return "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, grp := range apiGroups.Groups {
|
for _, grp := range apiGroups.Groups {
|
||||||
if grp.Name != group {
|
if grp.Name != group {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return grp.PreferredVersion.Version, true
|
return grp.PreferredVersion.Version, true, nil
|
||||||
|
|
||||||
// for _, version := range grp.Versions {
|
|
||||||
// for _, supportedVersion := range versions {
|
|
||||||
// if version.Version == supportedVersion {
|
|
||||||
// return supportedVersion, true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
return "", false, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,20 +147,17 @@ func (r *Container) List(ns string) (Columnars, error) {
|
||||||
|
|
||||||
// Header return resource header.
|
// Header return resource header.
|
||||||
func (*Container) Header(ns string) Row {
|
func (*Container) Header(ns string) Row {
|
||||||
hh := Row{}
|
return append(Row{},
|
||||||
|
|
||||||
return append(hh,
|
|
||||||
"NAME",
|
"NAME",
|
||||||
"IMAGE",
|
"IMAGE",
|
||||||
"READY",
|
"READY",
|
||||||
"STATE",
|
"STATE",
|
||||||
"RS",
|
"RS",
|
||||||
"LPROB",
|
"PROBES(L:R)",
|
||||||
"RPROB",
|
|
||||||
"CPU",
|
"CPU",
|
||||||
"MEM",
|
"MEM",
|
||||||
"RCPU",
|
"%CPU",
|
||||||
"RMEM",
|
"%MEM",
|
||||||
"AGE",
|
"AGE",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +167,7 @@ func (r *Container) Fields(ns string) Row {
|
||||||
ff := make(Row, 0, len(r.Header(ns)))
|
ff := make(Row, 0, len(r.Header(ns)))
|
||||||
i := r.instance
|
i := r.instance
|
||||||
|
|
||||||
scpu, smem := NAValue, NAValue
|
scpu, smem, pcpu, pmem := NAValue, NAValue, NAValue, NAValue
|
||||||
if r.metrics != nil {
|
if r.metrics != nil {
|
||||||
var (
|
var (
|
||||||
cpu int64
|
cpu int64
|
||||||
|
|
@ -184,8 +181,14 @@ func (r *Container) Fields(ns string) Row {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scpu, smem = ToMillicore(cpu), ToMi(mem)
|
scpu, smem = ToMillicore(cpu), ToMi(mem)
|
||||||
|
rcpu, rmem := containerResources(i)
|
||||||
|
if rcpu != nil {
|
||||||
|
pcpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue())))
|
||||||
|
}
|
||||||
|
if rmem != nil {
|
||||||
|
pmem = AsPerc(toPerc(mem, k8s.ToMB(rmem.Value())))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rcpu, rmem := resources(i)
|
|
||||||
|
|
||||||
var cs *v1.ContainerStatus
|
var cs *v1.ContainerStatus
|
||||||
for _, c := range r.pod.Status.ContainerStatuses {
|
for _, c := range r.pod.Status.ContainerStatuses {
|
||||||
|
|
@ -215,12 +218,11 @@ func (r *Container) Fields(ns string) Row {
|
||||||
ready,
|
ready,
|
||||||
state,
|
state,
|
||||||
restarts,
|
restarts,
|
||||||
probe(i.LivenessProbe),
|
probe(i.LivenessProbe)+":"+probe(i.ReadinessProbe),
|
||||||
probe(i.ReadinessProbe),
|
|
||||||
scpu,
|
scpu,
|
||||||
smem,
|
smem,
|
||||||
rcpu,
|
pcpu,
|
||||||
rmem,
|
pmem,
|
||||||
toAge(r.pod.CreationTimestamp),
|
toAge(r.pod.CreationTimestamp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -254,18 +256,6 @@ func toRes(r v1.ResourceList) (string, string) {
|
||||||
return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value()))
|
return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func resources(c v1.Container) (cpu, mem string) {
|
|
||||||
req, lim := c.Resources.Requests, c.Resources.Limits
|
|
||||||
if len(req) != 0 {
|
|
||||||
return toRes(req)
|
|
||||||
}
|
|
||||||
if len(lim) != 0 {
|
|
||||||
return toRes(lim)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NAValue, NAValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func probe(p *v1.Probe) string {
|
func probe(p *v1.Probe) string {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return "on"
|
return "on"
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ func join(a []string, sep string) string {
|
||||||
|
|
||||||
// AsPerc prints a number as a percentage.
|
// AsPerc prints a number as a percentage.
|
||||||
func AsPerc(f float64) string {
|
func AsPerc(f float64) string {
|
||||||
return strconv.Itoa(int(f)) + "%"
|
return strconv.Itoa(int(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToPerc computes the ratio of two numbers as a percentage.
|
// ToPerc computes the ratio of two numbers as a percentage.
|
||||||
|
|
@ -168,12 +168,12 @@ func mapToStr(m map[string]string) (s string) {
|
||||||
|
|
||||||
// ToMillicore shows cpu reading for human.
|
// ToMillicore shows cpu reading for human.
|
||||||
func ToMillicore(v int64) string {
|
func ToMillicore(v int64) string {
|
||||||
return strconv.Itoa(int(v)) + "m"
|
return strconv.Itoa(int(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMi shows mem reading for human.
|
// ToMi shows mem reading for human.
|
||||||
func ToMi(v float64) string {
|
func ToMi(v float64) string {
|
||||||
return strconv.Itoa(int(v)) + "Mi"
|
return strconv.Itoa(int(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
func boolPtrToStr(b *bool) string {
|
func boolPtrToStr(b *bool) string {
|
||||||
|
|
|
||||||
|
|
@ -142,9 +142,9 @@ func TestToMillicore(t *testing.T) {
|
||||||
v int64
|
v int64
|
||||||
e string
|
e string
|
||||||
}{
|
}{
|
||||||
{0, "0m"},
|
{0, "0"},
|
||||||
{2, "2m"},
|
{2, "2"},
|
||||||
{1000, "1000m"},
|
{1000, "1000"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for _, u := range uu {
|
||||||
|
|
@ -157,9 +157,9 @@ func TestToMi(t *testing.T) {
|
||||||
v float64
|
v float64
|
||||||
e string
|
e string
|
||||||
}{
|
}{
|
||||||
{0, "0Mi"},
|
{0, "0"},
|
||||||
{2, "2Mi"},
|
{2, "2"},
|
||||||
{1000, "1000Mi"},
|
{1000, "1000"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for _, u := range uu {
|
||||||
|
|
@ -172,10 +172,10 @@ func TestAsPerc(t *testing.T) {
|
||||||
v float64
|
v float64
|
||||||
e string
|
e string
|
||||||
}{
|
}{
|
||||||
{0, "0%"},
|
{0, "0"},
|
||||||
{10.5, "10%"},
|
{10.5, "10"},
|
||||||
{10, "10%"},
|
{10, "10"},
|
||||||
{0.05, "0%"},
|
{0.05, "0"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for _, u := range uu {
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,7 @@ func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) {
|
||||||
LabelSelector: l.resource.GetLabelSelector(),
|
LabelSelector: l.resource.GetLabelSelector(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Debug().Msgf(">>>>>> DOH! %#v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -288,14 +288,15 @@ func (mock *MockClusterMeta) ServerVersion() (*version.Info, error) {
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool) {
|
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
|
||||||
if mock == nil {
|
if mock == nil {
|
||||||
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
|
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
|
||||||
}
|
}
|
||||||
params := []pegomock.Param{_param0, _param1}
|
params := []pegomock.Param{_param0, _param1}
|
||||||
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
|
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
var ret0 string
|
var ret0 string
|
||||||
var ret1 bool
|
var ret1 bool
|
||||||
|
var ret2 error
|
||||||
if len(result) != 0 {
|
if len(result) != 0 {
|
||||||
if result[0] != nil {
|
if result[0] != nil {
|
||||||
ret0 = result[0].(string)
|
ret0 = result[0].(string)
|
||||||
|
|
@ -303,8 +304,11 @@ func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (stri
|
||||||
if result[1] != nil {
|
if result[1] != nil {
|
||||||
ret1 = result[1].(bool)
|
ret1 = result[1].(bool)
|
||||||
}
|
}
|
||||||
|
if result[2] != nil {
|
||||||
|
ret2 = result[2].(error)
|
||||||
}
|
}
|
||||||
return ret0, ret1
|
}
|
||||||
|
return ret0, ret1, ret2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {
|
func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {
|
||||||
|
|
|
||||||
|
|
@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) {
|
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
|
||||||
if mock == nil {
|
if mock == nil {
|
||||||
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
||||||
}
|
}
|
||||||
params := []pegomock.Param{_param0, _param1}
|
params := []pegomock.Param{_param0, _param1}
|
||||||
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
|
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
var ret0 string
|
var ret0 string
|
||||||
var ret1 bool
|
var ret1 bool
|
||||||
|
var ret2 error
|
||||||
if len(result) != 0 {
|
if len(result) != 0 {
|
||||||
if result[0] != nil {
|
if result[0] != nil {
|
||||||
ret0 = result[0].(string)
|
ret0 = result[0].(string)
|
||||||
|
|
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
|
||||||
if result[1] != nil {
|
if result[1] != nil {
|
||||||
ret1 = result[1].(bool)
|
ret1 = result[1].(bool)
|
||||||
}
|
}
|
||||||
|
if result[2] != nil {
|
||||||
|
ret2 = result[2].(error)
|
||||||
}
|
}
|
||||||
return ret0, ret1
|
}
|
||||||
|
return ret0, ret1, ret2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockConnection) SupportsResource(_param0 string) bool {
|
func (mock *MockConnection) SupportsResource(_param0 string) bool {
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ func (*Node) Header(ns string) Row {
|
||||||
"EXTERNAL-IP",
|
"EXTERNAL-IP",
|
||||||
"CPU",
|
"CPU",
|
||||||
"MEM",
|
"MEM",
|
||||||
|
"%CPU",
|
||||||
|
"%MEM",
|
||||||
"ACPU",
|
"ACPU",
|
||||||
"AMEM",
|
"AMEM",
|
||||||
"AGE",
|
"AGE",
|
||||||
|
|
@ -125,7 +127,7 @@ func (r *Node) Fields(ns string) Row {
|
||||||
iIP, eIP := r.getIPs(no.Status.Addresses)
|
iIP, eIP := r.getIPs(no.Status.Addresses)
|
||||||
iIP, eIP = missing(iIP), missing(eIP)
|
iIP, eIP = missing(iIP), missing(eIP)
|
||||||
|
|
||||||
ccpu, cmem, scpu, smem := NAValue, NAValue, NAValue, NAValue
|
ccpu, cmem, scpu, smem, pcpu, pmem := NAValue, NAValue, NAValue, NAValue, NAValue, NAValue
|
||||||
if r.metrics != nil {
|
if r.metrics != nil {
|
||||||
var (
|
var (
|
||||||
cpu int64
|
cpu int64
|
||||||
|
|
@ -137,8 +139,10 @@ func (r *Node) Fields(ns string) Row {
|
||||||
|
|
||||||
acpu := no.Status.Allocatable.Cpu().MilliValue()
|
acpu := no.Status.Allocatable.Cpu().MilliValue()
|
||||||
amem := k8s.ToMB(no.Status.Allocatable.Memory().Value())
|
amem := k8s.ToMB(no.Status.Allocatable.Memory().Value())
|
||||||
ccpu = withPerc(ToMillicore(cpu), AsPerc(toPerc(float64(cpu), float64(acpu))))
|
ccpu = ToMillicore(cpu)
|
||||||
cmem = withPerc(ToMi(mem), AsPerc(toPerc(mem, amem)))
|
pcpu = AsPerc(toPerc(float64(cpu), float64(acpu)))
|
||||||
|
cmem = ToMi(mem)
|
||||||
|
pmem = AsPerc(toPerc(mem, amem))
|
||||||
scpu = ToMillicore(cpu)
|
scpu = ToMillicore(cpu)
|
||||||
smem = ToMi(mem)
|
smem = ToMi(mem)
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +162,8 @@ func (r *Node) Fields(ns string) Row {
|
||||||
eIP,
|
eIP,
|
||||||
ccpu,
|
ccpu,
|
||||||
cmem,
|
cmem,
|
||||||
|
pcpu,
|
||||||
|
pmem,
|
||||||
scpu,
|
scpu,
|
||||||
smem,
|
smem,
|
||||||
toAge(no.ObjectMeta.CreationTimestamp),
|
toAge(no.ObjectMeta.CreationTimestamp),
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ func TestNodeListData(t *testing.T) {
|
||||||
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||||
row, ok := td.Rows["fred"]
|
row, ok := td.Rows["fred"]
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, 12, len(row.Deltas))
|
assert.Equal(t, 14, len(row.Deltas))
|
||||||
for _, d := range row.Deltas {
|
for _, d := range row.Deltas {
|
||||||
assert.Equal(t, "", d)
|
assert.Equal(t, "", d)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,21 +62,6 @@ func (r *Namespace) Marshal(path string) (string, error) {
|
||||||
return r.marshalObject(nss)
|
return r.marshalObject(nss)
|
||||||
}
|
}
|
||||||
|
|
||||||
// // List resources for a given namespace.
|
|
||||||
// func (r *Namespace) List(ns string) (Columnars, error) {
|
|
||||||
// r.Resource.
|
|
||||||
// nss, err := r.Resource.List(ns)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// cc := make(Columnars, 0, len(nss))
|
|
||||||
// for i := range nss {
|
|
||||||
// cc = append(cc, r.New(&nss[i]).(*Namespace))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return cc, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Header returns resource header.
|
// Header returns resource header.
|
||||||
func (*Namespace) Header(ns string) Row {
|
func (*Namespace) Header(ns string) Row {
|
||||||
return Row{"NAME", "STATUS", "AGE"}
|
return Row{"NAME", "STATUS", "AGE"}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
"k8s.io/kubernetes/pkg/util/node"
|
"k8s.io/kubernetes/pkg/util/node"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
@ -198,6 +199,8 @@ func (*Pod) Header(ns string) Row {
|
||||||
"RS",
|
"RS",
|
||||||
"CPU",
|
"CPU",
|
||||||
"MEM",
|
"MEM",
|
||||||
|
"%CPU",
|
||||||
|
"%MEM",
|
||||||
"IP",
|
"IP",
|
||||||
"NODE",
|
"NODE",
|
||||||
"QOS",
|
"QOS",
|
||||||
|
|
@ -217,17 +220,13 @@ func (r *Pod) Fields(ns string) Row {
|
||||||
ss := i.Status.ContainerStatuses
|
ss := i.Status.ContainerStatuses
|
||||||
cr, _, rc := r.statuses(ss)
|
cr, _, rc := r.statuses(ss)
|
||||||
|
|
||||||
scpu, smem := NAValue, NAValue
|
ccpu, cmem, pcpu, pmem := NAValue, NAValue, NAValue, NAValue
|
||||||
if r.metrics != nil {
|
if r.metrics != nil {
|
||||||
var cpu int64
|
c, m := r.currentRes(r.metrics)
|
||||||
var mem float64
|
ccpu, cmem = ToMillicore(c.MilliValue()), ToMi(k8s.ToMB(m.Value()))
|
||||||
|
rc, rm := r.requestedRes(i)
|
||||||
for _, c := range r.metrics.Containers {
|
pcpu = AsPerc(toPerc(float64(c.MilliValue()), float64(rc.MilliValue())))
|
||||||
cpu += c.Usage.Cpu().MilliValue()
|
pmem = AsPerc(toPerc(k8s.ToMB(m.Value()), k8s.ToMB(rm.Value())))
|
||||||
mem += k8s.ToMB(c.Usage.Memory().Value())
|
|
||||||
}
|
|
||||||
scpu = ToMillicore(cpu)
|
|
||||||
smem = ToMi(mem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(ff,
|
return append(ff,
|
||||||
|
|
@ -235,8 +234,10 @@ func (r *Pod) Fields(ns string) Row {
|
||||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||||
r.phase(i),
|
r.phase(i),
|
||||||
strconv.Itoa(rc),
|
strconv.Itoa(rc),
|
||||||
scpu,
|
ccpu,
|
||||||
smem,
|
cmem,
|
||||||
|
pcpu,
|
||||||
|
pmem,
|
||||||
i.Status.PodIP,
|
i.Status.PodIP,
|
||||||
i.Spec.NodeName,
|
i.Spec.NodeName,
|
||||||
r.mapQOS(i.Status.QOSClass),
|
r.mapQOS(i.Status.QOSClass),
|
||||||
|
|
@ -247,6 +248,41 @@ func (r *Pod) Fields(ns string) Row {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
func containerResources(co v1.Container) (cpu, mem *resource.Quantity) {
|
||||||
|
req, limit := co.Resources.Requests, co.Resources.Limits
|
||||||
|
switch {
|
||||||
|
case len(req) != 0 && len(limit) != 0:
|
||||||
|
cpu, mem = limit.Cpu(), limit.Memory()
|
||||||
|
case len(req) != 0:
|
||||||
|
cpu, mem = req.Cpu(), req.Memory()
|
||||||
|
case len(limit) != 0:
|
||||||
|
cpu, mem = limit.Cpu(), limit.Memory()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Pod) requestedRes(po *v1.Pod) (cpu, mem resource.Quantity) {
|
||||||
|
for _, co := range po.Spec.Containers {
|
||||||
|
c, m := containerResources(co)
|
||||||
|
if c != nil {
|
||||||
|
cpu.Add(*c)
|
||||||
|
}
|
||||||
|
if m != nil {
|
||||||
|
mem.Add(*m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Pod) currentRes(mx *mv1beta1.PodMetrics) (cpu, mem resource.Quantity) {
|
||||||
|
for _, co := range mx.Containers {
|
||||||
|
c, m := co.Usage.Cpu(), co.Usage.Memory()
|
||||||
|
cpu.Add(*c)
|
||||||
|
mem.Add(*m)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (*Pod) mapQOS(class v1.PodQOSClass) string {
|
func (*Pod) mapQOS(class v1.PodQOSClass) string {
|
||||||
switch class {
|
switch class {
|
||||||
case v1.PodQOSGuaranteed:
|
case v1.PodQOSGuaranteed:
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ func TestPodListData(t *testing.T) {
|
||||||
assert.Equal(t, 1, len(td.Rows))
|
assert.Equal(t, 1, len(td.Rows))
|
||||||
assert.Equal(t, "blee", l.GetNamespace())
|
assert.Equal(t, "blee", l.GetNamespace())
|
||||||
row := td.Rows["blee/fred"]
|
row := td.Rows["blee/fred"]
|
||||||
assert.Equal(t, 10, len(row.Deltas))
|
assert.Equal(t, 12, len(row.Deltas))
|
||||||
for _, d := range row.Deltas {
|
for _, d := range row.Deltas {
|
||||||
assert.Equal(t, "", d)
|
assert.Equal(t, "", d)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,11 +216,14 @@ func (a *appView) Run() {
|
||||||
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
key := evt.Key()
|
key := evt.Key()
|
||||||
if key == tcell.KeyRune {
|
if key == tcell.KeyRune {
|
||||||
if a.cmdBuff.isActive() {
|
if a.cmdBuff.isActive() && evt.Modifiers() == tcell.ModNone {
|
||||||
a.cmdBuff.add(evt.Rune())
|
a.cmdBuff.add(evt.Rune())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
key = tcell.Key(evt.Rune())
|
key = tcell.Key(evt.Rune())
|
||||||
|
if evt.Modifiers() == tcell.ModAlt {
|
||||||
|
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if a, ok := a.actions[key]; ok {
|
if a, ok := a.actions[key]; ok {
|
||||||
|
|
@ -340,7 +343,6 @@ func (a *appView) inject(i igniter) {
|
||||||
a.cancel()
|
a.cancel()
|
||||||
}
|
}
|
||||||
a.content.RemovePage("main")
|
a.content.RemovePage("main")
|
||||||
|
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
{
|
{
|
||||||
ctx, a.cancel = context.WithCancel(context.Background())
|
ctx, a.cancel = context.WithCancel(context.Background())
|
||||||
|
|
|
||||||
|
|
@ -124,16 +124,22 @@ func (v *clusterInfoView) refresh() {
|
||||||
cluster.Metrics(nos, nmx, &cmx)
|
cluster.Metrics(nos, nmx, &cmx)
|
||||||
c = v.GetCell(row, 1)
|
c = v.GetCell(row, 1)
|
||||||
cpu := resource.AsPerc(cmx.PercCPU)
|
cpu := resource.AsPerc(cmx.PercCPU)
|
||||||
|
if cpu == "0" {
|
||||||
|
cpu = resource.NAValue
|
||||||
|
}
|
||||||
c.SetText(cpu + deltas(strip(c.Text), cpu))
|
c.SetText(cpu + deltas(strip(c.Text), cpu))
|
||||||
row++
|
row++
|
||||||
|
|
||||||
c = v.GetCell(row, 1)
|
c = v.GetCell(row, 1)
|
||||||
mem := resource.AsPerc(cmx.PercMEM)
|
mem := resource.AsPerc(cmx.PercMEM)
|
||||||
|
if mem == "0" {
|
||||||
|
mem = resource.NAValue
|
||||||
|
}
|
||||||
c.SetText(mem + deltas(strip(c.Text), mem))
|
c.SetText(mem + deltas(strip(c.Text), mem))
|
||||||
}
|
}
|
||||||
|
|
||||||
func strip(s string) string {
|
func strip(s string) string {
|
||||||
t := strings.Replace(s, plus(), "", 1)
|
t := strings.Replace(s, plusSign, "", 1)
|
||||||
t = strings.Replace(t, minus(), "", 1)
|
t = strings.Replace(t, minusSign, "", 1)
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,16 @@ type containerView struct {
|
||||||
*resourceView
|
*resourceView
|
||||||
|
|
||||||
current igniter
|
current igniter
|
||||||
|
exitFn func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContainerView(t string, app *appView, list resource.List, path string) resourceViewer {
|
func newContainerView(t string, app *appView, list resource.List, path string, exitFn func()) resourceViewer {
|
||||||
v := containerView{resourceView: newResourceView(t, app, list).(*resourceView)}
|
v := containerView{resourceView: newResourceView(t, app, list).(*resourceView)}
|
||||||
{
|
{
|
||||||
v.path = &path
|
v.path = &path
|
||||||
v.extraActionsFn = v.extraActions
|
v.extraActionsFn = v.extraActions
|
||||||
v.current = app.content.GetPrimitive("main").(igniter)
|
v.current = app.content.GetPrimitive("main").(igniter)
|
||||||
|
v.exitFn = exitFn
|
||||||
}
|
}
|
||||||
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
|
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
|
||||||
v.switchPage("co")
|
v.switchPage("co")
|
||||||
|
|
@ -97,13 +99,15 @@ func (v *containerView) shellIn(path, co string) {
|
||||||
|
|
||||||
func (v *containerView) extraActions(aa keyActions) {
|
func (v *containerView) extraActions(aa keyActions) {
|
||||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||||
aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true)
|
aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
|
||||||
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
||||||
aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false)
|
aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false)
|
||||||
aa[KeyP] = newKeyAction("Previous", v.backCmd, false)
|
aa[KeyP] = newKeyAction("Previous", v.backCmd, false)
|
||||||
aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false)
|
aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false)
|
||||||
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true)
|
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, false), true)
|
||||||
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true)
|
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(7, false), true)
|
||||||
|
aa[KeyAltC] = newKeyAction("Sort %CPU", v.sortColCmd(8, false), true)
|
||||||
|
aa[KeyAltM] = newKeyAction("Sort %MEM", v.sortColCmd(9, false), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
@ -117,7 +121,8 @@ func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
v.app.inject(v.current)
|
// v.app.inject(v.current)
|
||||||
|
v.exitFn()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,27 @@ func (v *flashView) setMessage(level flashLevel, msg ...string) {
|
||||||
if v.cancel != nil {
|
if v.cancel != nil {
|
||||||
v.cancel()
|
v.cancel()
|
||||||
}
|
}
|
||||||
|
var ctx1, ctx2 context.Context
|
||||||
|
{
|
||||||
|
ctx1, v.cancel = context.WithCancel(context.TODO())
|
||||||
|
ctx2, _ = context.WithTimeout(context.TODO(), flashDelay*time.Second)
|
||||||
|
go func(ctx1, ctx2 context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// Timer canceled bail now
|
||||||
|
case <-ctx1.Done():
|
||||||
|
return
|
||||||
|
// Timed out clear and bail
|
||||||
|
case <-ctx2.Done():
|
||||||
|
v.app.QueueUpdateDraw(func() {
|
||||||
|
v.Clear()
|
||||||
|
v.app.Draw()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(ctx1, ctx2)
|
||||||
|
}
|
||||||
_, _, width, _ := v.GetRect()
|
_, _, width, _ := v.GetRect()
|
||||||
if width <= 15 {
|
if width <= 15 {
|
||||||
width = 100
|
width = 100
|
||||||
|
|
@ -56,23 +76,6 @@ func (v *flashView) setMessage(level flashLevel, msg ...string) {
|
||||||
m := strings.Join(msg, " ")
|
m := strings.Join(msg, " ")
|
||||||
v.SetTextColor(flashColor(level))
|
v.SetTextColor(flashColor(level))
|
||||||
v.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3))
|
v.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||||
|
|
||||||
var ctx context.Context
|
|
||||||
{
|
|
||||||
ctx, v.cancel = context.WithTimeout(context.TODO(), flashDelay*time.Second)
|
|
||||||
go func(ctx context.Context) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
v.app.QueueUpdateDraw(func() {
|
|
||||||
v.Clear()
|
|
||||||
// v.app.Draw()
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(ctx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func flashEmoji(l flashLevel) string {
|
func flashEmoji(l flashLevel) string {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,15 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// deltaSign = "𝜟"
|
||||||
|
// plusSign = "⬆"
|
||||||
|
// minusSign = "⬇︎"
|
||||||
|
deltaSign = "Δ"
|
||||||
|
plusSign = "↑"
|
||||||
|
minusSign = "↓"
|
||||||
|
)
|
||||||
|
|
||||||
func deltas(o, n string) string {
|
func deltas(o, n string) string {
|
||||||
o, n = strings.TrimSpace(o), strings.TrimSpace(n)
|
o, n = strings.TrimSpace(o), strings.TrimSpace(n)
|
||||||
if o == "" || o == res.NAValue {
|
if o == "" || o == res.NAValue {
|
||||||
|
|
@ -20,9 +29,9 @@ func deltas(o, n string) string {
|
||||||
j, _ := numerical(n)
|
j, _ := numerical(n)
|
||||||
switch {
|
switch {
|
||||||
case i < j:
|
case i < j:
|
||||||
return plus()
|
return plusSign
|
||||||
case i > j:
|
case i > j:
|
||||||
return minus()
|
return minusSign
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -32,9 +41,9 @@ func deltas(o, n string) string {
|
||||||
j, _ := percentage(n)
|
j, _ := percentage(n)
|
||||||
switch {
|
switch {
|
||||||
case i < j:
|
case i < j:
|
||||||
return plus()
|
return plusSign
|
||||||
case i > j:
|
case i > j:
|
||||||
return minus()
|
return minusSign
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -44,9 +53,9 @@ func deltas(o, n string) string {
|
||||||
q2, _ := resource.ParseQuantity(n)
|
q2, _ := resource.ParseQuantity(n)
|
||||||
switch q1.Cmp(q2) {
|
switch q1.Cmp(q2) {
|
||||||
case -1:
|
case -1:
|
||||||
return plus()
|
return plusSign
|
||||||
case 1:
|
case 1:
|
||||||
return minus()
|
return minusSign
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -56,9 +65,9 @@ func deltas(o, n string) string {
|
||||||
d2, _ := time.ParseDuration(n)
|
d2, _ := time.ParseDuration(n)
|
||||||
switch {
|
switch {
|
||||||
case d2-d1 > 0:
|
case d2-d1 > 0:
|
||||||
return plus()
|
return plusSign
|
||||||
case d2-d1 < 0:
|
case d2-d1 < 0:
|
||||||
return minus()
|
return minusSign
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +75,7 @@ func deltas(o, n string) string {
|
||||||
|
|
||||||
switch strings.Compare(o, n) {
|
switch strings.Compare(o, n) {
|
||||||
case 1, -1:
|
case 1, -1:
|
||||||
return delta()
|
return deltaSign
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -91,15 +100,3 @@ func numerical(s string) (int, bool) {
|
||||||
|
|
||||||
return n, true
|
return n, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func delta() string {
|
|
||||||
return "𝜟"
|
|
||||||
}
|
|
||||||
|
|
||||||
func plus() string {
|
|
||||||
return "⬆"
|
|
||||||
}
|
|
||||||
|
|
||||||
func minus() string {
|
|
||||||
return "⬇︎"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -17,22 +17,22 @@ func TestDeltas(t *testing.T) {
|
||||||
s1, s2, e string
|
s1, s2, e string
|
||||||
}{
|
}{
|
||||||
{"", "", ""},
|
{"", "", ""},
|
||||||
{resource.MissingValue, "", delta()},
|
{resource.MissingValue, "", deltaSign},
|
||||||
{resource.NAValue, "", ""},
|
{resource.NAValue, "", ""},
|
||||||
{"fred", "fred", ""},
|
{"fred", "fred", ""},
|
||||||
{"fred", "blee", delta()},
|
{"fred", "blee", deltaSign},
|
||||||
{"1", "1", ""},
|
{"1", "1", ""},
|
||||||
{"1", "2", plus()},
|
{"1", "2", plusSign},
|
||||||
{"2", "1", minus()},
|
{"2", "1", minusSign},
|
||||||
{"2m33s", "2m33s", ""},
|
{"2m33s", "2m33s", ""},
|
||||||
{"2m33s", "1m", minus()},
|
{"2m33s", "1m", minusSign},
|
||||||
{"33s", "1m", plus()},
|
{"33s", "1m", plusSign},
|
||||||
{"10Gi", "10Gi", ""},
|
{"10Gi", "10Gi", ""},
|
||||||
{"10Gi", "20Gi", plus()},
|
{"10Gi", "20Gi", plusSign},
|
||||||
{"30Gi", "20Gi", minus()},
|
{"30Gi", "20Gi", minusSign},
|
||||||
{"15%", "15%", ""},
|
{"15%", "15%", ""},
|
||||||
{"20%", "40%", plus()},
|
{"20%", "40%", plusSign},
|
||||||
{"5%", "2%", minus()},
|
{"5%", "2%", minusSign},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for _, u := range uu {
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ func (v *jobView) showLogs(path, co, view string, parent loggable, prev bool) {
|
||||||
|
|
||||||
func (v *jobView) extraActions(aa keyActions) {
|
func (v *jobView) extraActions(aa keyActions) {
|
||||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||||
aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true)
|
aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
|
||||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,45 +6,154 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type logView struct {
|
type logView struct {
|
||||||
*detailsView
|
*tview.Flex
|
||||||
|
|
||||||
|
app *appView
|
||||||
|
logs *detailsView
|
||||||
|
status *statusView
|
||||||
|
parent masterView
|
||||||
ansiWriter io.Writer
|
ansiWriter io.Writer
|
||||||
|
autoScroll bool
|
||||||
|
actions keyActions
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLogView(title string, parent loggable) *logView {
|
func newLogView(title string, parent masterView) *logView {
|
||||||
v := logView{detailsView: newDetailsView(parent.appView(), parent.backFn())}
|
v := logView{Flex: tview.NewFlex(), app: parent.appView()}
|
||||||
{
|
v.autoScroll = true
|
||||||
|
v.parent = parent
|
||||||
|
v.SetBorder(true)
|
||||||
v.SetBorderPadding(0, 0, 1, 1)
|
v.SetBorderPadding(0, 0, 1, 1)
|
||||||
v.setCategory("Logs")
|
v.logs = newDetailsView(parent.appView(), parent.backFn())
|
||||||
v.SetDynamicColors(true)
|
{
|
||||||
v.SetWrap(true)
|
v.logs.SetBorder(false)
|
||||||
v.setTitle(parent.getSelection())
|
v.logs.setCategory("Logs")
|
||||||
v.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize)
|
v.logs.SetDynamicColors(true)
|
||||||
v.ansiWriter = tview.ANSIWriter(v)
|
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
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logView) logLine(line string) {
|
// Hints show action hints
|
||||||
fmt.Fprintln(l.ansiWriter, tview.Escape(line))
|
func (v *logView) hints() hints {
|
||||||
|
return v.actions.toHints()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logView) log(lines fmt.Stringer) {
|
func (v *logView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
l.Clear()
|
key := evt.Key()
|
||||||
fmt.Fprintln(l.ansiWriter, lines.String())
|
if key == tcell.KeyRune {
|
||||||
|
key = tcell.Key(evt.Rune())
|
||||||
|
}
|
||||||
|
if m, ok := v.actions[key]; ok {
|
||||||
|
log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key])
|
||||||
|
return m.action(evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logView) flush(index int, buff []string, scroll bool) {
|
return evt
|
||||||
if index > 0 {
|
}
|
||||||
l.logLine(strings.Join(buff[:index], "\n"))
|
|
||||||
if scroll {
|
func (v *logView) logLine(line string) {
|
||||||
l.app.QueueUpdate(func() {
|
fmt.Fprintln(v.ansiWriter, tview.Escape(line))
|
||||||
l.ScrollToEnd()
|
}
|
||||||
|
|
||||||
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/resource"
|
"github.com/derailed/k9s/internal/resource"
|
||||||
|
|
@ -20,6 +20,11 @@ const (
|
||||||
flushTimeout = 200 * time.Millisecond
|
flushTimeout = 200 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type masterView interface {
|
||||||
|
backFn() actionHandler
|
||||||
|
appView() *appView
|
||||||
|
}
|
||||||
|
|
||||||
type logsView struct {
|
type logsView struct {
|
||||||
*tview.Pages
|
*tview.Pages
|
||||||
|
|
||||||
|
|
@ -28,7 +33,6 @@ type logsView struct {
|
||||||
containers []string
|
containers []string
|
||||||
actions keyActions
|
actions keyActions
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
autoScroll bool
|
|
||||||
showPrevious bool
|
showPrevious bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,19 +41,8 @@ func newLogsView(pview string, parent loggable) *logsView {
|
||||||
Pages: tview.NewPages(),
|
Pages: tview.NewPages(),
|
||||||
parent: parent,
|
parent: parent,
|
||||||
parentView: pview,
|
parentView: pview,
|
||||||
autoScroll: true,
|
|
||||||
containers: []string{},
|
containers: []string{},
|
||||||
}
|
}
|
||||||
v.setActions(keyActions{
|
|
||||||
tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true},
|
|
||||||
KeyC: {description: "Clear", action: v.clearCmd, visible: true},
|
|
||||||
KeyS: {description: "Toggle AutoScroll", action: v.toggleScrollCmd, visible: true},
|
|
||||||
KeyG: {description: "Top", action: v.topCmd, visible: false},
|
|
||||||
KeyShiftG: {description: "Bottom", action: v.bottomCmd, visible: false},
|
|
||||||
KeyF: {description: "Up", action: v.pageUpCmd, visible: false},
|
|
||||||
KeyB: {description: "Down", action: v.pageDownCmd, visible: false},
|
|
||||||
})
|
|
||||||
v.SetInputCapture(v.keyboard)
|
|
||||||
|
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
@ -63,35 +56,6 @@ func (v *logsView) reload(co string, parent loggable, view string, prevLogs bool
|
||||||
v.load(0)
|
v.load(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|
||||||
key := evt.Key()
|
|
||||||
if key == tcell.KeyRune {
|
|
||||||
key = tcell.Key(evt.Rune())
|
|
||||||
}
|
|
||||||
|
|
||||||
if kv, ok := v.CurrentPage().Item.(keyHandler); ok {
|
|
||||||
if kv.keyboard(evt) == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if evt.Key() == tcell.KeyRune {
|
|
||||||
if i, err := strconv.Atoi(string(evt.Rune())); err == nil {
|
|
||||||
if _, ok := numKeys[i]; ok {
|
|
||||||
v.load(i - 1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m, ok := v.actions[key]; ok {
|
|
||||||
log.Debug().Msgf(">> LogsView handled %s", tcell.KeyNames[key])
|
|
||||||
return m.action(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
return evt
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetActions to handle keyboard events.
|
// SetActions to handle keyboard events.
|
||||||
func (v *logsView) setActions(aa keyActions) {
|
func (v *logsView) setActions(aa keyActions) {
|
||||||
v.actions = aa
|
v.actions = aa
|
||||||
|
|
@ -99,25 +63,24 @@ func (v *logsView) setActions(aa keyActions) {
|
||||||
|
|
||||||
// Hints show action hints
|
// Hints show action hints
|
||||||
func (v *logsView) hints() hints {
|
func (v *logsView) hints() hints {
|
||||||
if len(v.containers) > 1 {
|
l := v.CurrentPage().Item.(*logView)
|
||||||
for i, c := range v.containers {
|
return l.actions.toHints()
|
||||||
v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.actions.toHints()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logsView) addContainer(n string) {
|
func (v *logsView) addContainer(n string) {
|
||||||
v.containers = append(v.containers, n)
|
v.containers = append(v.containers, n)
|
||||||
l := newLogView(n, v.parent)
|
l := newLogView(n, v)
|
||||||
{
|
|
||||||
l.SetInputCapture(v.keyboard)
|
|
||||||
l.backFn = v.backCmd
|
|
||||||
}
|
|
||||||
v.AddPage(n, l, true, false)
|
v.AddPage(n, l, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *logsView) appView() *appView {
|
||||||
|
return v.parent.appView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *logsView) backFn() actionHandler {
|
||||||
|
return v.backCmd
|
||||||
|
}
|
||||||
|
|
||||||
func (v *logsView) deleteAllPages() {
|
func (v *logsView) deleteAllPages() {
|
||||||
for i, c := range v.containers {
|
for i, c := range v.containers {
|
||||||
v.RemovePage(c)
|
v.RemovePage(c)
|
||||||
|
|
@ -130,7 +93,6 @@ func (v *logsView) stop() {
|
||||||
if v.cancelFunc == nil {
|
if v.cancelFunc == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
v.cancelFunc()
|
v.cancelFunc()
|
||||||
log.Debug().Msgf("Canceling logs...")
|
log.Debug().Msgf("Canceling logs...")
|
||||||
v.cancelFunc = nil
|
v.cancelFunc = nil
|
||||||
|
|
@ -140,7 +102,6 @@ func (v *logsView) load(i int) {
|
||||||
if i < 0 || i > len(v.containers)-1 {
|
if i < 0 || i > len(v.containers)-1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
v.SwitchToPage(v.containers[i])
|
v.SwitchToPage(v.containers[i])
|
||||||
if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
|
if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
|
||||||
v.parent.appView().flash(flashErr, err.Error())
|
v.parent.appView().flash(flashErr, err.Error())
|
||||||
|
|
@ -156,8 +117,12 @@ func (v *logsView) doLoad(path, co string) error {
|
||||||
|
|
||||||
maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize)
|
maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize)
|
||||||
l := v.CurrentPage().Item.(*logView)
|
l := v.CurrentPage().Item.(*logView)
|
||||||
l.Clear()
|
l.logs.Clear()
|
||||||
l.setTitle(path + ":" + co)
|
const logFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:-:-]) "
|
||||||
|
fmat := fmt.Sprintf(logFmt, path, co)
|
||||||
|
fmat = strings.Replace(fmat, "[fg:bg", "["+v.parent.appView().styles.Style.Title.FgColor+":"+v.parent.appView().styles.Style.Title.BgColor, -1)
|
||||||
|
fmat = strings.Replace(fmat, "[hilite", "["+v.parent.appView().styles.Style.Title.HighlightColor, 1)
|
||||||
|
l.SetTitle(fmat)
|
||||||
|
|
||||||
c := make(chan string, 10)
|
c := make(chan string, 10)
|
||||||
go func(l *logView) {
|
go func(l *logView) {
|
||||||
|
|
@ -166,7 +131,7 @@ func (v *logsView) doLoad(path, co string) error {
|
||||||
select {
|
select {
|
||||||
case line, ok := <-c:
|
case line, ok := <-c:
|
||||||
if !ok {
|
if !ok {
|
||||||
l.flush(index, buff, v.autoScroll)
|
l.flush(index, buff)
|
||||||
index = 0
|
index = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -175,11 +140,11 @@ func (v *logsView) doLoad(path, co string) error {
|
||||||
index++
|
index++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l.flush(index, buff, v.autoScroll)
|
l.flush(index, buff)
|
||||||
index = 0
|
index = 0
|
||||||
buff[index] = line
|
buff[index] = line
|
||||||
case <-time.After(flushTimeout):
|
case <-time.After(flushTimeout):
|
||||||
l.flush(index, buff, v.autoScroll)
|
l.flush(index, buff)
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,67 +169,9 @@ func (v *logsView) doLoad(path, co string) error {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Actions...
|
// Actions...
|
||||||
|
|
||||||
func (v *logsView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
||||||
v.autoScroll = !v.autoScroll
|
|
||||||
if v.autoScroll {
|
|
||||||
v.parent.appView().flash(flashInfo, "Autoscroll is on.")
|
|
||||||
} else {
|
|
||||||
v.parent.appView().flash(flashInfo, "Autoscroll is off.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *logsView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *logsView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
v.stop()
|
v.stop()
|
||||||
v.parent.switchPage(v.parentView)
|
v.parent.switchPage(v.parentView)
|
||||||
|
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *logsView) topCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
||||||
if p := v.CurrentPage(); p != nil {
|
|
||||||
v.parent.appView().flash(flashInfo, "Top of logs...")
|
|
||||||
p.Item.(*logView).ScrollToBeginning()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *logsView) bottomCmd(*tcell.EventKey) *tcell.EventKey {
|
|
||||||
if p := v.CurrentPage(); p != nil {
|
|
||||||
v.parent.appView().flash(flashInfo, "Bottom of logs...")
|
|
||||||
p.Item.(*logView).ScrollToEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *logsView) pageUpCmd(*tcell.EventKey) *tcell.EventKey {
|
|
||||||
if p := v.CurrentPage(); p != nil {
|
|
||||||
if p.Item.(*logView).PageUp() {
|
|
||||||
v.parent.appView().flash(flashInfo, "Reached Top ...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *logsView) pageDownCmd(*tcell.EventKey) *tcell.EventKey {
|
|
||||||
if p := v.CurrentPage(); p != nil {
|
|
||||||
if p.Item.(*logView).PageDown() {
|
|
||||||
v.parent.appView().flash(flashInfo, "Reached Bottom ...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *logsView) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
|
||||||
if p := v.CurrentPage(); p != nil {
|
|
||||||
v.parent.appView().flash(flashInfo, "Clearing logs...")
|
|
||||||
p.Item.(*logView).Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,36 @@ const (
|
||||||
Key9
|
Key9
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines AltKeys
|
||||||
|
const (
|
||||||
|
KeyAltA tcell.Key = 4 * (iota + 97)
|
||||||
|
KeyAltB
|
||||||
|
KeyAltC
|
||||||
|
KeyAltD
|
||||||
|
KeyAltE
|
||||||
|
KeyAltF
|
||||||
|
KeyAltG
|
||||||
|
KeyAltH
|
||||||
|
KeyAltI
|
||||||
|
KeyAltJ
|
||||||
|
KeyAltK
|
||||||
|
KeyAltL
|
||||||
|
KeyAltM
|
||||||
|
KeyAltN
|
||||||
|
KeyAltO
|
||||||
|
KeyAltP
|
||||||
|
KeyAltQ
|
||||||
|
KeyAltR
|
||||||
|
KeyAltS
|
||||||
|
KeyAltT
|
||||||
|
KeyAltU
|
||||||
|
KeyAltV
|
||||||
|
KeyAltW
|
||||||
|
KeyAltX
|
||||||
|
KeyAltY
|
||||||
|
KeyAltZ
|
||||||
|
)
|
||||||
|
|
||||||
// Defines char keystrokes
|
// Defines char keystrokes
|
||||||
const (
|
const (
|
||||||
KeyA tcell.Key = iota + 97
|
KeyA tcell.Key = iota + 97
|
||||||
|
|
@ -351,4 +381,32 @@ func initKeys() {
|
||||||
|
|
||||||
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
|
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
|
||||||
tcell.KeyNames[tcell.Key(KeySlash)] = "/"
|
tcell.KeyNames[tcell.Key(KeySlash)] = "/"
|
||||||
|
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltA)] = "ALT-A"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltB)] = "ALT-B"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltC)] = "ALT-C"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltD)] = "ALT-D"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltE)] = "ALT-E"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltF)] = "ALT-F"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltG)] = "ALT-G"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltH)] = "ALT-H"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltI)] = "ALT-I"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltJ)] = "ALT-J"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltK)] = "ALT-K"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltL)] = "ALT-L"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltM)] = "ALT-M"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltN)] = "ALT-N"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltO)] = "ALT-O"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltP)] = "ALT-P"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltQ)] = "ALT-Q"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltR)] = "ALT-R"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltS)] = "ALT-S"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltT)] = "ALT-T"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltU)] = "ALT-U"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltV)] = "ALT-V"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltW)] = "ALT-W"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltX)] = "ALT-X"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltY)] = "ALT-Y"
|
||||||
|
tcell.KeyNames[tcell.Key(KeyAltZ)] = "ALT-Z"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,14 +288,15 @@ func (mock *MockClusterMeta) ServerVersion() (*version.Info, error) {
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool) {
|
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
|
||||||
if mock == nil {
|
if mock == nil {
|
||||||
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
|
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
|
||||||
}
|
}
|
||||||
params := []pegomock.Param{_param0, _param1}
|
params := []pegomock.Param{_param0, _param1}
|
||||||
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
|
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
var ret0 string
|
var ret0 string
|
||||||
var ret1 bool
|
var ret1 bool
|
||||||
|
var ret2 error
|
||||||
if len(result) != 0 {
|
if len(result) != 0 {
|
||||||
if result[0] != nil {
|
if result[0] != nil {
|
||||||
ret0 = result[0].(string)
|
ret0 = result[0].(string)
|
||||||
|
|
@ -303,8 +304,11 @@ func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (stri
|
||||||
if result[1] != nil {
|
if result[1] != nil {
|
||||||
ret1 = result[1].(bool)
|
ret1 = result[1].(bool)
|
||||||
}
|
}
|
||||||
|
if result[2] != nil {
|
||||||
|
ret2 = result[2].(error)
|
||||||
}
|
}
|
||||||
return ret0, ret1
|
}
|
||||||
|
return ret0, ret1, ret2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {
|
func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -17,6 +18,8 @@ const containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
|
||||||
|
|
||||||
type podView struct {
|
type podView struct {
|
||||||
*resourceView
|
*resourceView
|
||||||
|
|
||||||
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggable interface {
|
type loggable interface {
|
||||||
|
|
@ -33,7 +36,6 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer {
|
||||||
v.extraActionsFn = v.extraActions
|
v.extraActionsFn = v.extraActions
|
||||||
v.enterFn = v.listContainers
|
v.enterFn = v.listContainers
|
||||||
}
|
}
|
||||||
|
|
||||||
picker := newSelectList(&v)
|
picker := newSelectList(&v)
|
||||||
{
|
{
|
||||||
picker.setActions(keyActions{
|
picker.setActions(keyActions{
|
||||||
|
|
@ -41,7 +43,6 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
v.AddPage("picker", picker, true, false)
|
v.AddPage("picker", picker, true, false)
|
||||||
|
|
||||||
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
|
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
|
||||||
v.switchPage("po")
|
v.switchPage("po")
|
||||||
|
|
||||||
|
|
@ -52,14 +53,12 @@ func (v *podView) listContainers(app *appView, _, res, sel string) {
|
||||||
if !v.rowSelected() {
|
if !v.rowSelected() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{})
|
po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel)
|
log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel)
|
||||||
app.flash(flashErr, err.Error())
|
app.flash(flashErr, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pod := po.(*v1.Pod)
|
pod := po.(*v1.Pod)
|
||||||
mx := k8s.NewMetricsServer(app.conn())
|
mx := k8s.NewMetricsServer(app.conn())
|
||||||
list := resource.NewContainerList(app.conn(), mx, pod)
|
list := resource.NewContainerList(app.conn(), mx, pod)
|
||||||
|
|
@ -68,12 +67,20 @@ func (v *podView) listContainers(app *appView, _, res, sel string) {
|
||||||
fmat := strings.Replace(containerFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
fmat := strings.Replace(containerFmt, "[fg:bg", "["+v.app.styles.Style.Title.FgColor+":"+v.app.styles.Style.Title.BgColor, -1)
|
||||||
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
|
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
|
||||||
title := fmt.Sprintf(fmat, "Containers", sel)
|
title := fmt.Sprintf(fmat, "Containers", sel)
|
||||||
app.inject(newContainerView(
|
|
||||||
title,
|
v.suspend()
|
||||||
app,
|
cv := newContainerView(title, app, list, namespacedName(pod.Namespace, pod.Name), v.exitFn)
|
||||||
list,
|
v.AddPage("containers", cv, true, true)
|
||||||
namespacedName(pod.Namespace, pod.Name),
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
))
|
v.cancel = cancel
|
||||||
|
cv.init(ctx, pod.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *podView) exitFn() {
|
||||||
|
v.cancel()
|
||||||
|
v.switchPage("po")
|
||||||
|
v.RemovePage("containers")
|
||||||
|
v.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protocol...
|
// Protocol...
|
||||||
|
|
@ -116,19 +123,16 @@ func (v *podView) viewLogs(prev bool) bool {
|
||||||
if !v.rowSelected() {
|
if !v.rowSelected() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
cc, err := fetchContainers(v.list, v.selectedItem, true)
|
cc, err := fetchContainers(v.list, v.selectedItem, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash(flashErr, err.Error())
|
||||||
log.Error().Err(err)
|
log.Error().Err(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cc) == 1 {
|
if len(cc) == 1 {
|
||||||
v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, prev)
|
v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, prev)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
picker := v.GetPrimitive("picker").(*selectList)
|
picker := v.GetPrimitive("picker").(*selectList)
|
||||||
picker.populate(cc)
|
picker.populate(cc)
|
||||||
picker.SetSelectedFunc(func(i int, t, d string, r rune) {
|
picker.SetSelectedFunc(func(i int, t, d string, r rune) {
|
||||||
|
|
@ -142,7 +146,6 @@ func (v *podView) viewLogs(prev bool) bool {
|
||||||
func (v *podView) showLogs(path, co, view string, parent loggable, prev bool) {
|
func (v *podView) showLogs(path, co, view string, parent loggable, prev bool) {
|
||||||
l := v.GetPrimitive("logs").(*logsView)
|
l := v.GetPrimitive("logs").(*logsView)
|
||||||
l.reload(co, parent, view, prev)
|
l.reload(co, parent, view, prev)
|
||||||
|
|
||||||
v.switchPage("logs")
|
v.switchPage("logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,19 +153,16 @@ func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if !v.rowSelected() {
|
if !v.rowSelected() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
cc, err := fetchContainers(v.list, v.selectedItem, false)
|
cc, err := fetchContainers(v.list, v.selectedItem, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash(flashErr, err.Error())
|
||||||
log.Error().Msgf("Error fetching containers %v", err)
|
log.Error().Msgf("Error fetching containers %v", err)
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cc) == 1 {
|
if len(cc) == 1 {
|
||||||
v.shellIn(v.selectedItem, "")
|
v.shellIn(v.selectedItem, "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
p := v.GetPrimitive("picker").(*selectList)
|
p := v.GetPrimitive("picker").(*selectList)
|
||||||
p.populate(cc)
|
p.populate(cc)
|
||||||
p.SetSelectedFunc(func(i int, t, d string, r rune) {
|
p.SetSelectedFunc(func(i int, t, d string, r rune) {
|
||||||
|
|
@ -194,14 +194,16 @@ func (v *podView) shellIn(path, co string) {
|
||||||
|
|
||||||
func (v *podView) extraActions(aa keyActions) {
|
func (v *podView) extraActions(aa keyActions) {
|
||||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||||
aa[KeyShiftL] = newKeyAction("Logs", v.prevLogsCmd, true)
|
aa[KeyShiftL] = newKeyAction("Prev Logs", v.prevLogsCmd, true)
|
||||||
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
||||||
aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true)
|
aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true)
|
||||||
aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true)
|
aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true)
|
||||||
aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true)
|
aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true)
|
||||||
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true)
|
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true)
|
||||||
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true)
|
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true)
|
||||||
aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(7, true), true)
|
aa[KeyAltC] = newKeyAction("Sort %CPU", v.sortColCmd(6, false), true)
|
||||||
|
aa[KeyAltM] = newKeyAction("Sort %MEM", v.sortColCmd(7, false), true)
|
||||||
|
aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(8, true), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import (
|
||||||
type (
|
type (
|
||||||
viewFn func(ns string, app *appView, list resource.List) resourceViewer
|
viewFn func(ns string, app *appView, list resource.List) resourceViewer
|
||||||
listFn func(c resource.Connection, ns string) resource.List
|
listFn func(c resource.Connection, ns string) resource.List
|
||||||
// listMxFn func(c resource.Connection, mx resource.MetricsServer, ns string) resource.List
|
|
||||||
colorerFn func(ns string, evt *resource.RowEvent) tcell.Color
|
colorerFn func(ns string, evt *resource.RowEvent) tcell.Color
|
||||||
enterFn func(app *appView, ns, resource, selection string)
|
enterFn func(app *appView, ns, resource, selection string)
|
||||||
decorateFn func(resource.TableData) resource.TableData
|
decorateFn func(resource.TableData) resource.TableData
|
||||||
|
|
@ -21,7 +20,6 @@ type (
|
||||||
api string
|
api string
|
||||||
viewFn viewFn
|
viewFn viewFn
|
||||||
listFn listFn
|
listFn listFn
|
||||||
// listMxFn listMxFn
|
|
||||||
enterFn enterFn
|
enterFn enterFn
|
||||||
colorerFn colorerFn
|
colorerFn colorerFn
|
||||||
decorateFn decorateFn
|
decorateFn decorateFn
|
||||||
|
|
@ -297,9 +295,14 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rev, ok := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"})
|
rev, ok, err := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Checking HPA")
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Warn().Msg("HPA are not supported on this cluster")
|
log.Error().Msg("HPA are not supported on this cluster")
|
||||||
|
return cmds
|
||||||
}
|
}
|
||||||
|
|
||||||
switch rev {
|
switch rev {
|
||||||
|
|
@ -328,7 +331,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
||||||
listFn: resource.NewHorizontalPodAutoscalerList,
|
listFn: resource.NewHorizontalPodAutoscalerList,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
log.Panic().Msgf("K9s does not currently support HPA version `%s`", rev)
|
log.Panic().Msgf("K9s unsupported HPA version. Exiting!")
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmds
|
return cmds
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,15 @@ func (v *resourceView) init(ctx context.Context, ns string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *resourceView) reloadList(list resource.List, ns string) {
|
||||||
|
v.suspend()
|
||||||
|
{
|
||||||
|
v.list = list
|
||||||
|
v.list.SetNamespace(ns)
|
||||||
|
}
|
||||||
|
v.resume()
|
||||||
|
}
|
||||||
|
|
||||||
func (v *resourceView) updater(ctx context.Context) {
|
func (v *resourceView) updater(ctx context.Context) {
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
for {
|
for {
|
||||||
|
|
@ -388,7 +397,6 @@ func (v *resourceView) refresh() {
|
||||||
}
|
}
|
||||||
|
|
||||||
v.refreshActions()
|
v.refreshActions()
|
||||||
|
|
||||||
if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
|
if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
|
||||||
log.Error().Err(err).Msg("Reconciliation failed")
|
log.Error().Err(err).Msg("Reconciliation failed")
|
||||||
v.app.flash(flashErr, err.Error())
|
v.app.flash(flashErr, err.Error())
|
||||||
|
|
@ -443,6 +451,7 @@ func (v *resourceView) switchPage(p string) {
|
||||||
v.app.setHints(h.hints())
|
v.app.setHints(h.hints())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("Current page %#v", v.CurrentPage())
|
||||||
if _, ok := v.CurrentPage().Item.(*tableView); ok {
|
if _, ok := v.CurrentPage().Item.(*tableView); ok {
|
||||||
v.resume()
|
v.resume()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:-]] "
|
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] "
|
||||||
searchFmt = "<[filter:bg:b]/%s[fg:bg:]> "
|
searchFmt = "<[filter:bg:b]/%s[fg:bg:]> "
|
||||||
nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
|
nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
|
||||||
|
descIndicator = "↓"
|
||||||
|
ascIndicator = "↑"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
crx = regexp.MustCompile(`\A.{0,1}CPU`)
|
||||||
|
mrx = regexp.MustCompile(`\A.{0,1}MEM`)
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
@ -110,6 +117,9 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
key = tcell.Key(evt.Rune())
|
key = tcell.Key(evt.Rune())
|
||||||
|
if evt.Modifiers() == tcell.ModAlt {
|
||||||
|
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if a, ok := v.actions[key]; ok {
|
if a, ok := v.actions[key]; ok {
|
||||||
|
|
@ -181,8 +191,8 @@ func (v *tableView) sortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKe
|
||||||
} else {
|
} else {
|
||||||
v.sortCol.index, v.sortCol.asc = v.nameColIndex()+col, true
|
v.sortCol.index, v.sortCol.asc = v.nameColIndex()+col, true
|
||||||
}
|
}
|
||||||
|
|
||||||
v.refresh()
|
v.refresh()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,9 +299,9 @@ func (v *tableView) sortIndicator(index int, name string) string {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
order := "↓"
|
order := descIndicator
|
||||||
if v.sortCol.asc {
|
if v.sortCol.asc {
|
||||||
order = "↑"
|
order = ascIndicator
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s[%s::]%s[::]", name, v.app.styles.Style.Table.Header.SorterColor, order)
|
return fmt.Sprintf("%s[%s::]%s[::]", name, v.app.styles.Style.Table.Header.SorterColor, order)
|
||||||
}
|
}
|
||||||
|
|
@ -324,7 +334,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
|
||||||
fg := config.AsColor(v.app.styles.Style.Table.Header.FgColor)
|
fg := config.AsColor(v.app.styles.Style.Table.Header.FgColor)
|
||||||
bg := config.AsColor(v.app.styles.Style.Table.Header.BgColor)
|
bg := config.AsColor(v.app.styles.Style.Table.Header.BgColor)
|
||||||
for col, h := range data.Header {
|
for col, h := range data.Header {
|
||||||
v.addHeaderCell(col, h, pads, fg, bg)
|
v.addHeaderCell(col, h, fg, bg)
|
||||||
}
|
}
|
||||||
row++
|
row++
|
||||||
|
|
||||||
|
|
@ -340,11 +350,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
|
||||||
fgColor = v.colorerFn(data.Namespace, data.Rows[sk])
|
fgColor = v.colorerFn(data.Namespace, data.Rows[sk])
|
||||||
}
|
}
|
||||||
for col, field := range data.Rows[sk].Fields {
|
for col, field := range data.Rows[sk].Fields {
|
||||||
var age bool
|
v.addBodyCell(data.Header[col], row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
|
||||||
if data.Header[col] == "AGE" {
|
|
||||||
age = true
|
|
||||||
}
|
|
||||||
v.addBodyCell(age, row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
|
|
||||||
}
|
}
|
||||||
row++
|
row++
|
||||||
}
|
}
|
||||||
|
|
@ -372,34 +378,43 @@ func (v *tableView) sortAllRows(rows resource.RowEvents, sortFn sortFn) (resourc
|
||||||
return prim, sec
|
return prim, sec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *tableView) addHeaderCell(col int, name string, pads maxyPad, fg, bg tcell.Color) {
|
func (v *tableView) addHeaderCell(col int, name string, fg, bg tcell.Color) {
|
||||||
c := tview.NewTableCell(v.sortIndicator(col, name))
|
c := tview.NewTableCell(v.sortIndicator(col, name))
|
||||||
{
|
{
|
||||||
c.SetExpansion(1)
|
c.SetExpansion(1)
|
||||||
c.SetTextColor(fg)
|
c.SetTextColor(fg)
|
||||||
|
if crx.MatchString(name) || mrx.MatchString(name) {
|
||||||
|
c.SetAlign(tview.AlignRight)
|
||||||
|
}
|
||||||
c.SetBackgroundColor(bg)
|
c.SetBackgroundColor(bg)
|
||||||
}
|
}
|
||||||
v.SetCell(0, col, c)
|
v.SetCell(0, col, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *tableView) addBodyCell(age bool, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
|
func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
|
||||||
dField := field
|
const colPadding = 3
|
||||||
if age {
|
|
||||||
|
if header == "AGE" {
|
||||||
dur, err := time.ParseDuration(field)
|
dur, err := time.ParseDuration(field)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dField = duration.HumanDuration(dur)
|
field = duration.HumanDuration(dur)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dField += deltas(delta, field)
|
field += deltas(delta, field)
|
||||||
if isASCII(field) {
|
align := tview.AlignLeft
|
||||||
dField = pad(dField, pads[col]+5)
|
if crx.MatchString(header) || mrx.MatchString(header) {
|
||||||
|
align = tview.AlignRight
|
||||||
|
} else if isASCII(field) {
|
||||||
|
field = pad(field, pads[col]+colPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := tview.NewTableCell(dField)
|
c := tview.NewTableCell(field)
|
||||||
{
|
{
|
||||||
c.SetExpansion(1)
|
c.SetExpansion(1)
|
||||||
|
c.SetAlign(align)
|
||||||
c.SetTextColor(color)
|
c.SetTextColor(color)
|
||||||
|
c.SetMaxWidth(pads[col] + colPadding)
|
||||||
}
|
}
|
||||||
v.SetCell(row, col, c)
|
v.SetCell(row, col, c)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,15 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AllNamespaces designates all namespaces.
|
||||||
|
const AllNamespaces = ""
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Row represents a collection of string fields.
|
// Row represents a collection of string fields.
|
||||||
Row []string
|
Row []string
|
||||||
|
|
@ -53,13 +57,30 @@ type Meta struct {
|
||||||
// NewMeta creates a new cluster resource informer
|
// NewMeta creates a new cluster resource informer
|
||||||
func NewMeta(client k8s.Connection, ns string) *Meta {
|
func NewMeta(client k8s.Connection, ns string) *Meta {
|
||||||
m := Meta{client: client, informers: map[string]StoreInformer{}}
|
m := Meta{client: client, informers: map[string]StoreInformer{}}
|
||||||
m.init("")
|
|
||||||
|
nsAccess := m.client.CanIAccess("", "", "namespaces", []string{"list", "watch"})
|
||||||
|
ns, err := client.Config().CurrentNamespaceName()
|
||||||
|
// User did not lock NS. Check all ns access if not bail
|
||||||
|
if err != nil && !nsAccess {
|
||||||
|
log.Panic().Msg("Unauthorized access to list namespaces. Please specify a namespace")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace is locks in check if user has auth for this ns access.
|
||||||
|
if ns != AllNamespaces && !nsAccess {
|
||||||
|
if !m.client.CanIAccess("", ns, "namespaces", []string{"get", "watch"}) {
|
||||||
|
log.Panic().Msgf("Unauthorized access to namespace %q", ns)
|
||||||
|
}
|
||||||
|
m.init(ns)
|
||||||
|
} else {
|
||||||
|
m.init(AllNamespaces)
|
||||||
|
}
|
||||||
|
|
||||||
return &m
|
return &m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Meta) init(ns string) {
|
func (m *Meta) init(ns string) {
|
||||||
po := NewPod(m.client, ns)
|
po := NewPod(m.client, ns)
|
||||||
|
|
||||||
m.informers = map[string]StoreInformer{
|
m.informers = map[string]StoreInformer{
|
||||||
NodeIndex: NewNode(m.client),
|
NodeIndex: NewNode(m.client),
|
||||||
PodIndex: po,
|
PodIndex: po,
|
||||||
|
|
@ -67,17 +88,19 @@ func (m *Meta) init(ns string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.client.HasMetrics() {
|
if m.client.HasMetrics() {
|
||||||
|
if m.client.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"}) {
|
||||||
m.informers[NodeMXIndex] = NewNodeMetrics(m.client)
|
m.informers[NodeMXIndex] = NewNodeMetrics(m.client)
|
||||||
m.informers[PodMXIndex] = NewPodMetrics(m.client, ns)
|
m.informers[PodMXIndex] = NewPodMetrics(m.client, ns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CheckAccess checks if current user as enought RBAC fu to access watched resources.
|
// CheckAccess checks if current user as enought RBAC fu to access watched resources.
|
||||||
func (m *Meta) checkAccess(ns string) error {
|
func (m *Meta) checkAccess(ns string) error {
|
||||||
if !m.client.CanIAccess(ns, "nodes", "node.v1", []string{"list", "watch"}) {
|
if !m.client.CanIAccess(ns, "nodes", "nodes", []string{"list", "watch"}) {
|
||||||
return fmt.Errorf("Not authorized to list/watch nodes")
|
return fmt.Errorf("Not authorized to list/watch nodes")
|
||||||
}
|
}
|
||||||
if !m.client.CanIAccess(ns, "pods", "pod.v1", []string{"list", "watch"}) {
|
if !m.client.CanIAccess(ns, "pods", "pods", []string{"list", "watch"}) {
|
||||||
return fmt.Errorf("Not authorized to list/watch pods in namespace %s", ns)
|
return fmt.Errorf("Not authorized to list/watch pods in namespace %s", ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,51 +3,64 @@ package watch
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
m "github.com/petergtz/pegomock"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMetaList(t *testing.T) {
|
func TestMetaList(t *testing.T) {
|
||||||
|
f := new(genericclioptions.ConfigFlags)
|
||||||
cmo := NewMockConnection()
|
cmo := NewMockConnection()
|
||||||
m := NewMeta(cmo, "")
|
m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
|
||||||
|
meta := NewMeta(cmo, "")
|
||||||
|
|
||||||
o, err := m.List(PodIndex, "fred", metav1.ListOptions{})
|
o, err := meta.List(PodIndex, "fred", metav1.ListOptions{})
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Assert(t, len(o) == 0)
|
assert.Assert(t, len(o) == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetaListNoRes(t *testing.T) {
|
func TestMetaListNoRes(t *testing.T) {
|
||||||
|
f := new(genericclioptions.ConfigFlags)
|
||||||
cmo := NewMockConnection()
|
cmo := NewMockConnection()
|
||||||
m := NewMeta(cmo, "")
|
m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
|
||||||
|
meta := NewMeta(cmo, "")
|
||||||
|
|
||||||
o, err := m.List("dp", "fred", metav1.ListOptions{})
|
o, err := meta.List("dp", "fred", metav1.ListOptions{})
|
||||||
assert.ErrorContains(t, err, "No informer found")
|
assert.ErrorContains(t, err, "No informer found")
|
||||||
assert.Assert(t, len(o) == 0)
|
assert.Assert(t, len(o) == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetaGet(t *testing.T) {
|
func TestMetaGet(t *testing.T) {
|
||||||
|
f := new(genericclioptions.ConfigFlags)
|
||||||
cmo := NewMockConnection()
|
cmo := NewMockConnection()
|
||||||
m := NewMeta(cmo, "")
|
m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
|
||||||
|
meta := NewMeta(cmo, "")
|
||||||
|
|
||||||
o, err := m.Get(PodIndex, "fred", metav1.GetOptions{})
|
o, err := meta.Get(PodIndex, "fred", metav1.GetOptions{})
|
||||||
assert.ErrorContains(t, err, "Pod fred not found")
|
assert.ErrorContains(t, err, "Pod fred not found")
|
||||||
assert.Assert(t, o == nil)
|
assert.Assert(t, o == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetaGetNoRes(t *testing.T) {
|
func TestMetaGetNoRes(t *testing.T) {
|
||||||
|
f := new(genericclioptions.ConfigFlags)
|
||||||
cmo := NewMockConnection()
|
cmo := NewMockConnection()
|
||||||
m := NewMeta(cmo, "")
|
m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
|
||||||
|
meta := NewMeta(cmo, "")
|
||||||
|
|
||||||
o, err := m.Get("rs", "fred", metav1.GetOptions{})
|
o, err := meta.Get("rs", "fred", metav1.GetOptions{})
|
||||||
assert.ErrorContains(t, err, "No informer found")
|
assert.ErrorContains(t, err, "No informer found")
|
||||||
assert.Assert(t, o == nil)
|
assert.Assert(t, o == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetaRun(t *testing.T) {
|
func TestMetaRun(t *testing.T) {
|
||||||
|
f := new(genericclioptions.ConfigFlags)
|
||||||
cmo := NewMockConnection()
|
cmo := NewMockConnection()
|
||||||
m := NewMeta(cmo, "")
|
m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f))
|
||||||
|
meta := NewMeta(cmo, "")
|
||||||
|
|
||||||
c := make(chan struct{})
|
c := make(chan struct{})
|
||||||
m.Run(c)
|
meta.Run(c)
|
||||||
close(c)
|
close(c)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -239,14 +239,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) {
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) {
|
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) {
|
||||||
if mock == nil {
|
if mock == nil {
|
||||||
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
||||||
}
|
}
|
||||||
params := []pegomock.Param{_param0, _param1}
|
params := []pegomock.Param{_param0, _param1}
|
||||||
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
|
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||||
var ret0 string
|
var ret0 string
|
||||||
var ret1 bool
|
var ret1 bool
|
||||||
|
var ret2 error
|
||||||
if len(result) != 0 {
|
if len(result) != 0 {
|
||||||
if result[0] != nil {
|
if result[0] != nil {
|
||||||
ret0 = result[0].(string)
|
ret0 = result[0].(string)
|
||||||
|
|
@ -254,8 +255,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin
|
||||||
if result[1] != nil {
|
if result[1] != nil {
|
||||||
ret1 = result[1].(bool)
|
ret1 = result[1].(bool)
|
||||||
}
|
}
|
||||||
|
if result[2] != nil {
|
||||||
|
ret2 = result[2].(error)
|
||||||
}
|
}
|
||||||
return ret0, ret1
|
}
|
||||||
|
return ret0, ret1, ret2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mock *MockConnection) SupportsResource(_param0 string) bool {
|
func (mock *MockConnection) SupportsResource(_param0 string) bool {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ func newNodeMetricsInformer(client k8s.Connection, sync time.Duration, idxs cach
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
l, err := c.MetricsV1beta1().NodeMetricses().List(opts)
|
l, err := c.MetricsV1beta1().NodeMetricses().List(opts)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pw.update(l, false)
|
pw.update(l, false)
|
||||||
|
|
@ -113,7 +112,6 @@ func (n *nodeMxWatcher) Run() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Fetch node metrics")
|
log.Error().Err(err).Msg("Fetch node metrics")
|
||||||
|
|
@ -142,7 +140,6 @@ func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) {
|
||||||
fqn := MetaFQN(list.Items[i].ObjectMeta)
|
fqn := MetaFQN(list.Items[i].ObjectMeta)
|
||||||
fqns[fqn] = &list.Items[i]
|
fqns[fqn] = &list.Items[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range n.cache {
|
for k, v := range n.cache {
|
||||||
if _, ok := fqns[k]; !ok {
|
if _, ok := fqns[k]; !ok {
|
||||||
if notify {
|
if notify {
|
||||||
|
|
@ -154,7 +151,6 @@ func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) {
|
||||||
delete(n.cache, k)
|
delete(n.cache, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range fqns {
|
for k, v := range fqns {
|
||||||
kind := watch.Added
|
kind := watch.Added
|
||||||
if v1, ok := n.cache[k]; ok {
|
if v1, ok := n.cache[k]; ok {
|
||||||
|
|
@ -163,7 +159,6 @@ func (n *nodeMxWatcher) update(list *mv1beta1.NodeMetricsList, notify bool) {
|
||||||
}
|
}
|
||||||
kind = watch.Modified
|
kind = watch.Modified
|
||||||
}
|
}
|
||||||
|
|
||||||
if notify {
|
if notify {
|
||||||
n.eventChan <- watch.Event{
|
n.eventChan <- watch.Event{
|
||||||
Type: kind,
|
Type: kind,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package watch
|
package watch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -75,11 +74,6 @@ func newPodMetricsInformer(client k8s.Connection, ns string, sync time.Duration,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !client.HasMetrics() {
|
|
||||||
return nil, errors.New("metrics-server not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := c.MetricsV1beta1().PodMetricses(ns).List(opts)
|
l, err := c.MetricsV1beta1().PodMetricses(ns).List(opts)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pw.update(l, false)
|
pw.update(l, false)
|
||||||
|
|
|
||||||
5
main.go
5
main.go
|
|
@ -2,14 +2,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/cmd"
|
"github.com/derailed/k9s/cmd"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||||
"k8s.io/klog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -18,9 +16,6 @@ func init() {
|
||||||
mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY
|
mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY
|
||||||
if file, err := os.OpenFile(config.K9sLogs, mod, config.DefaultFileMod); err == nil {
|
if file, err := os.OpenFile(config.K9sLogs, mod, config.DefaultFileMod); err == nil {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
|
||||||
// Klogs (of course) want to print stuff to the screen ;(
|
|
||||||
klog.SetOutput(file)
|
|
||||||
syscall.Dup2(int(file.Fd()), 2)
|
|
||||||
} else {
|
} else {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue