fix context/namespace support + bug fixes + added crumbs

mine
derailed 2019-03-15 16:10:22 -06:00
parent 57c77ca557
commit 0721e66a05
44 changed files with 1318 additions and 572 deletions

104
README.md
View File

@ -80,10 +80,17 @@ k9s --context coolCtx
```yaml
k9s:
# Indicates api-server poll intervals.
refreshRate: 2
# Indicates log view maximum buffer size. Default 1k lines.
logBufferSize: 200
# Indicates how many lines of logs to retrieve from the api-server. Default 200 lines.
logRequestSize: 200
# Indicates the current kube context. Defaults to current context
currentContext: minikube
# Indicates the current kube cluster. Defaults to current context cluster
currentCluster: minikube
# Persists per cluster preferences for favorite namespaces and view.
clusters:
bitchn:
namespace:
@ -143,12 +150,103 @@ K9s uses aliases to navigate most K8s resources.
This initial drop is brittle. K9s will most likely blow up...
1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.10+
1. You don't have enough RBAC fu to manage your cluster
1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.10+.
1. You don't have enough RBAC fu to manage your cluster (see RBAC section below).
1. Your cluster does not run a metric server.
---
## K9s RBAC FU
On RBAC enabled clusters, you would need to give your users/groups capabilities so that they can use K9s to explore Kubernetes cluster.
K9s needs minimaly read privileges at both the cluster and namespace level to display resources and metrics.
These rules below are just suggestions. You will need to customize them based on your environment policies. If you need to edit/delete resources extra Fu will be necessary.
> NOTE! Cluster/Namespace access may change in the future as K9s evolves.
> NOTE! We expect K9s to keep running even in atrophied clusters/namespaces. Please file issues if this is not the case!
### Cluster RBAC scope
```yaml
---
# K9s Reader ClusterRole
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: k9s
rules:
# Grants RO access to cluster resources node and namespace
- apiGroups: [""]
resources: ["nodes", "namespaces"]
verbs: ["get", "list"]
# Grants RO access to RBAC resources
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles", "roles", "clusterrolebindings", "rolebindings"]
verbs: ["get", "list"]
# Grants RO access to CRD resources
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list"]
# Grants RO access to netric server
- apiGroups: ["metrics.k8s.io"]
resources: ["nodes", "pods"]
verbs: ["list"]
---
# Sample K9s user ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: k9s
subjects:
- kind: User
name: fernand
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: k9s
apiGroup: rbac.authorization.k8s.io
```
### Namespace RBAC scope
If your users are constrained to certain namespaces, K9s will need to following role to enable read access to namespaced resources.
```yaml
---
# K9s Reader Role (default namespace)
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: k9s
namespace: default
rules:
# Grants RO access to most namespaced resources
- apiGroups: ["", "apps", "autoscaling", "batch", "extensions"]
resources: ["*"]
verbs: ["get", "list"]
---
# Sample K9s user RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: k9s
namespace: default
subjects:
- kind: User
name: fernand
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: k9s
apiGroup: rbac.authorization.k8s.io
```
---
## Disclaimer
This is still work in progress! If there is enough interest in the Kubernetes
@ -169,7 +267,7 @@ to make this project a reality!
## Contact Info
1. **Email**: fernand@imhotep.io
1. **Twitter**: [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
2. **Twitter**: [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
---

View File

@ -0,0 +1,30 @@
# Release v0.2.5
## 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!!
---
## Change Logs
+ Added an actual help view to show available key bindings. Use `<?>` to access it.
+ Changed alias view to now be accessible via key `<a>`
+ Pressing `<enter>` while on the namespace/context views will navigate directly to the pods view.
+ Added resource view breadcrumbs to easily navigate back in history. Use key `<p>` to navigate back.
+ Added configuration `logBufferSize` to limit the size of the log view while viewing chatty or big logs.
---
## Resolved Bugs
+ [Issue #116](https://github.com/derailed/k9s/issues/116)
+ [Issue #113](https://github.com/derailed/k9s/issues/113)
+ [Issue #111](https://github.com/derailed/k9s/issues/111)
+ [Issue #110](https://github.com/derailed/k9s/issues/110)

View File

@ -2,9 +2,9 @@ package cmd
import (
"fmt"
"strings"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/printer"
"github.com/spf13/cobra"
)
@ -14,18 +14,11 @@ func infoCmd() *cobra.Command {
Short: "Print configuration information",
Long: "Print configuration information",
Run: func(cmd *cobra.Command, args []string) {
const (
cyan = "\033[1;36m%s\033[0m"
green = "\033[1;32m%s\033[0m"
magenta = "\033[1;35m%s\033[0m"
)
fmt.Printf(cyan+"\n", strings.Repeat("-", 80))
fmt.Printf(green+"\n", "🐶 K9s Information")
fmt.Printf(magenta, fmt.Sprintf("%-10s", "LogFile:"))
fmt.Printf("%s\n", config.K9sLogs)
fmt.Printf(magenta, fmt.Sprintf("%-10s", "Config:"))
fmt.Printf("%s\n", config.K9sConfigFile)
fmt.Printf(cyan+"\n", strings.Repeat("-", 80))
fmt.Printf(printer.Colorize(fmt.Sprintf("%-15s", "Configuration:"), printer.ColorMagenta))
fmt.Println(printer.Colorize(config.K9sConfigFile, printer.ColorDarkGray))
fmt.Printf(printer.Colorize(fmt.Sprintf("%-15s", "Logs:"), printer.ColorMagenta))
fmt.Println(printer.Colorize(config.K9sLogs, printer.ColorDarkGray))
},
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/printer"
"github.com/derailed/k9s/internal/views"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -14,22 +15,23 @@ import (
)
const (
appName = "k9s"
defaultRefreshRate = 2 // secs
defaultLogLevel = "info"
shortAppDesc = "A graphical CLI for your Kubernetes cluster management."
longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters."
)
var (
version = "dev"
commit = "dev"
date = "n/a"
refreshRate int
logLevel string
k8sFlags *genericclioptions.ConfigFlags
version, commit, date = "dev", "dev", "n/a"
refreshRate int
logLevel string
k8sFlags *genericclioptions.ConfigFlags
rootCmd = &cobra.Command{
Use: "k9s",
Short: "A graphical CLI for your Kubernetes cluster management.",
Long: `K9s is a CLI to view and manage your Kubernetes clusters.`,
Use: appName,
Short: shortAppDesc,
Long: longAppDesc,
Run: run,
}
_ config.KubeSettings = &k8s.Config{}
@ -37,63 +39,77 @@ var (
func init() {
rootCmd.AddCommand(versionCmd(), infoCmd())
rootCmd.Flags().IntVarP(
&refreshRate,
"refresh", "r",
defaultRefreshRate,
"Specifies the default refresh rate as an integer (sec)",
)
rootCmd.Flags().StringVarP(
&logLevel,
"logLevel", "l",
defaultLogLevel,
"Specify a log level (info, warn, debug, error, fatal, panic, trace)",
)
initK9sFlags()
initK8sFlags()
}
func initK9s() {
// Execute root command
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Panic().Err(err)
}
}
func run(cmd *cobra.Command, args []string) {
defer func() {
clearScreen()
if err := recover(); err != nil {
log.Error().Msgf("%v", err)
log.Error().Msg(string(debug.Stack()))
fmt.Printf(printer.Colorize("Boom!! ", printer.ColorRed))
fmt.Println(printer.Colorize(fmt.Sprintf("%v.", err), printer.ColorDarkGray))
// debug.PrintStack()
}
}()
zerolog.SetGlobalLevel(parseLevel(logLevel))
loadConfiguration()
app := views.NewApp()
{
defer app.Stop()
app.Init(version, refreshRate, k8sFlags)
app.Run()
}
}
func loadConfiguration() {
log.Info().Msg("🐶 K9s starting up...")
// Load K9s config file...
cfg := k8s.NewConfig(k8sFlags)
config.Root = config.NewConfig(cfg)
initK9sConfig()
// Init K8s connection...
k8s.InitConnectionOrDie(cfg)
log.Info().Msg("✅ Kubernetes connectivity")
config.Root.Save()
}
func initK9sConfig() {
if err := config.Root.Load(config.K9sConfigFile); err != nil {
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
}
config.Root.K9s.RefreshRate = refreshRate
mergeConfigs()
// Init K8s connection...
k8s.InitConnectionOrDie(cfg)
log.Info().Msg("✅ Kubernetes connectivity")
config.Root.Save()
}
func mergeConfigs() {
cfg, err := k8sFlags.ToRawKubeConfigLoader().RawConfig()
if err != nil {
panic("Invalid configuration. Unable to connect to api")
}
ctx := cfg.CurrentContext
switch {
case isSet(k8sFlags.Context):
ctx = *k8sFlags.Context
config.Root.K9s.CurrentContext = ctx
case isSet(&config.Root.K9s.CurrentContext):
k8sFlags.Context = &config.Root.K9s.CurrentContext
default:
config.Root.K9s.CurrentContext = ctx
if isSet(&cfg.Contexts[ctx].Namespace) {
config.Root.SetActiveNamespace(cfg.Contexts[ctx].Namespace)
}
if isSet(k8sFlags.Context) {
config.Root.K9s.CurrentContext = *k8sFlags.Context
} else {
config.Root.K9s.CurrentContext = cfg.CurrentContext
}
log.Debug().Msgf("Active Context `%v`", config.Root.K9s.CurrentContext)
if c, ok := cfg.Contexts[config.Root.K9s.CurrentContext]; ok {
config.Root.K9s.CurrentCluster = c.Cluster
if len(c.Namespace) != 0 {
config.Root.SetActiveNamespace(c.Namespace)
}
} else {
log.Panic().Msg(fmt.Sprintf("The specified context `%s does not exists in kubeconfig", config.Root.K9s.CurrentContext))
}
log.Debug().Msgf("Active Context `%v`", ctx)
if isSet(k8sFlags.Namespace) {
config.Root.SetActiveNamespace(*k8sFlags.Namespace)
@ -102,26 +118,6 @@ func initK9sConfig() {
if isSet(k8sFlags.ClusterName) {
config.Root.K9s.CurrentCluster = *k8sFlags.ClusterName
}
if c, ok := cfg.Contexts[ctx]; ok {
config.Root.K9s.CurrentCluster = c.Cluster
if len(c.Namespace) != 0 {
config.Root.SetActiveNamespace(c.Namespace)
}
} else {
panic(fmt.Sprintf("The specified context `%s does not exists in kubeconfig", cfg.CurrentContext))
}
}
func isSet(s *string) bool {
return s != nil && len(*s) > 0
}
// Execute root command
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Panic().Err(err)
}
}
func parseLevel(level string) zerolog.Level {
@ -139,28 +135,19 @@ func parseLevel(level string) zerolog.Level {
}
}
func run(cmd *cobra.Command, args []string) {
zerolog.SetGlobalLevel(parseLevel(logLevel))
initK9s()
app := views.NewApp()
{
app.Init(version, refreshRate, k8sFlags)
defer func() {
clearScreen()
if err := recover(); err != nil {
app.Stop()
log.Error().Msgf("Boom! %#v", err)
debug.PrintStack()
}
}()
app.Run()
}
}
func clearScreen() {
fmt.Print("\033[H\033[2J")
func initK9sFlags() {
rootCmd.Flags().IntVarP(
&refreshRate,
"refresh", "r",
defaultRefreshRate,
"Specifies the default refresh rate as an integer (sec)",
)
rootCmd.Flags().StringVarP(
&logLevel,
"logLevel", "l",
defaultLogLevel,
"Specify a log level (info, warn, debug, error, fatal, panic, trace)",
)
}
func initK8sFlags() {
@ -257,3 +244,14 @@ func initK8sFlags() {
"If present, the namespace scope for this CLI request",
)
}
// ----------------------------------------------------------------------------
// Helpers...
func clearScreen() {
fmt.Print("\033[H\033[2J")
}
func isSet(s *string) bool {
return s != nil && len(*s) > 0
}

View File

@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"github.com/derailed/k9s/internal/printer"
"github.com/spf13/cobra"
)
@ -12,7 +13,13 @@ func versionCmd() *cobra.Command {
Short: "Print version info",
Long: "Prints version info",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Version:%s GitCommit:%s On %s\n", version, commit, date)
const secFmt = "%-10s"
fmt.Printf(printer.Colorize(fmt.Sprintf(secFmt, "Version:"), printer.ColorMagenta))
fmt.Println(printer.Colorize(version, printer.ColorDarkGray))
fmt.Printf(printer.Colorize(fmt.Sprintf(secFmt, "Commit:"), printer.ColorMagenta))
fmt.Println(printer.Colorize(commit, printer.ColorDarkGray))
fmt.Printf(printer.Colorize(fmt.Sprintf(secFmt, "Date:"), printer.ColorMagenta))
fmt.Println(printer.Colorize(date, printer.ColorDarkGray))
},
}
}

12
go.mod
View File

@ -1,30 +1,37 @@
module github.com/derailed/k9s
replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
require (
contrib.go.opencensus.io/exporter/ocagent v0.4.3 // indirect
github.com/Azure/go-autorest v11.4.0+incompatible // indirect
github.com/derailed/tview v0.1.3
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/evanphx/json-patch v4.1.0+incompatible // indirect
github.com/fatih/camelcase v1.0.0 // indirect
github.com/gdamore/tcell v1.1.1
github.com/gogo/protobuf v1.1.1 // indirect
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.5 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-runewidth v0.0.4
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/onsi/gomega v1.4.3 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/pborman/uuid v1.2.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81
github.com/rs/zerolog v1.12.0
github.com/sirupsen/logrus v1.3.0 // indirect
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.2.2
@ -32,9 +39,12 @@ require (
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd // indirect
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
gopkg.in/yaml.v2 v2.2.2
k8s.io/api v0.0.0-20190202010724-74b699b93c15
k8s.io/apiextensions-apiserver v0.0.0-20190313122605-80ebb0f65ac1 // indirect
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467
k8s.io/apiserver v0.0.0-20190313120755-39e839dff034 // indirect
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a
k8s.io/client-go v10.0.0+incompatible
k8s.io/klog v0.1.0 // indirect

24
go.sum
View File

@ -7,6 +7,7 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/Azure/go-autorest v11.4.0+incompatible h1:z3Yr6KYqs0nhSNwqGXEBpWK977hxVqsLv2n9PVYcixY=
github.com/Azure/go-autorest v11.4.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8 h1:gUqsFVdUKoRHNg8fkFd8gB5OOEa/g5EwlAHznb4zjbI=
github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -17,6 +18,8 @@ github.com/derailed/tview v0.1.3 h1:2/Rz0Sdfg3tepSKt4yCcY2g8IlRtPTrA4UYIQJZs6DI=
github.com/derailed/tview v0.1.3/go.mod h1:WRYVfgb2PBMLZ/muaSpOc/4H4fYsOPnHOaGnBoJ+hGE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc=
github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
@ -32,6 +35,8 @@ github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -43,6 +48,8 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 h1:wAcVWwS69gs5c6cFkCa/ns/eaL2gC761nF8Ugvd1dGw=
@ -52,6 +59,8 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway v1.6.2 h1:8KyC64BiO8ndiGHY5DlFWWdangUPC9QHPakFRre/Ud0=
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
@ -72,6 +81,7 @@ github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC1
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -81,8 +91,12 @@ github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81 h1:MhSbvsIs4KvpPYr4taOvb6j+r9VNbj/08AfjsKi+Ui0=
@ -90,12 +104,16 @@ github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81/go.mod h1:nuBLWZ
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181218105931-67670fe90761 h1:z6tvbDJ5OLJ48FFmnksv04a78maSTRBUIhkdHYV5Y98=
github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
@ -167,6 +185,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -176,8 +196,12 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20190202010724-74b699b93c15 h1:AoUGjnJ3PJMFz+Rkp4lx3X+6mPUnY1MESJhbUSGX+pc=
k8s.io/api v0.0.0-20190202010724-74b699b93c15/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apiextensions-apiserver v0.0.0-20190313122605-80ebb0f65ac1 h1:YfQqwXg6zropY1zGFmoKq/XlXW283XNmYoHS6lSOHcw=
k8s.io/apiextensions-apiserver v0.0.0-20190313122605-80ebb0f65ac1/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467 h1:zmz9UYvvXrK/B8EDqFuqreJEaXbIWdzEkNgWrN/Cd3o=
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/apiserver v0.0.0-20190313120755-39e839dff034 h1:I/bl2Ni4Cn6bsjPIIZZCiuAodcZgRijgkXm/2Z2EbDg=
k8s.io/apiserver v0.0.0-20190313120755-39e839dff034/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w=
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a h1:MrGQxLLZ09Bl5hYYU9VlKnhY60bpPlYd9yXOPnxkdc0=
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM=
k8s.io/client-go v10.0.0+incompatible h1:F1IqCqw7oMBzDkqlcBymRq1450wD0eNqLE9jzUrIi34=

View File

@ -141,6 +141,7 @@ func TestConfigSaveFile(t *testing.T) {
cfg.Load("test_assets/k9s.yml")
cfg.K9s.RefreshRate = 100
cfg.K9s.LogBufferSize = 500
cfg.K9s.LogRequestSize = 100
cfg.K9s.CurrentContext = "blee"
cfg.K9s.CurrentCluster = "blee"
cfg.Validate()
@ -190,6 +191,7 @@ func setup(t *testing.T) {
var expectedConfig = `k9s:
refreshRate: 100
logBufferSize: 500
logRequestSize: 100
currentContext: blee
currentCluster: blee
clusters:
@ -227,6 +229,7 @@ var expectedConfig = `k9s:
var resetConfig = `k9s:
refreshRate: 2
logBufferSize: 200
logRequestSize: 200
currentContext: blee
currentCluster: blee
clusters:

View File

@ -1,14 +1,16 @@
package config
const (
defaultRefreshRate = 2
defaultLogBufferSize = 200
defaultRefreshRate = 2
defaultLogRequestSize = 200
defaultLogBufferSize = 1000
)
// K9s tracks K9s configuration options.
type K9s struct {
RefreshRate int `yaml:"refreshRate"`
LogBufferSize int `yaml:"logBufferSize"`
LogRequestSize int `yaml:"logRequestSize"`
CurrentContext string `yaml:"currentContext"`
CurrentCluster string `yaml:"currentCluster"`
Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
@ -18,10 +20,11 @@ type K9s struct {
// NewK9s create a new K9s configuration.
func NewK9s() *K9s {
return &K9s{
RefreshRate: defaultRefreshRate,
LogBufferSize: defaultLogBufferSize,
Clusters: map[string]*Cluster{},
Aliases: map[string]string{},
RefreshRate: defaultRefreshRate,
LogBufferSize: defaultLogBufferSize,
LogRequestSize: defaultLogRequestSize,
Clusters: map[string]*Cluster{},
Aliases: map[string]string{},
}
}
@ -51,6 +54,10 @@ func (k *K9s) Validate(ks KubeSettings) {
k.LogBufferSize = defaultLogBufferSize
}
if k.LogRequestSize <= 0 {
k.LogRequestSize = defaultLogRequestSize
}
if k.Clusters == nil {
k.Clusters = map[string]*Cluster{}
}

View File

@ -20,7 +20,8 @@ func TestK9sValidate(t *testing.T) {
c.Validate(ksMock)
assert.Equal(t, 2, c.RefreshRate)
assert.Equal(t, 200, c.LogBufferSize)
assert.Equal(t, 1000, c.LogBufferSize)
assert.Equal(t, 200, c.LogRequestSize)
assert.Equal(t, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster)
assert.Equal(t, 1, len(c.Clusters))
@ -40,7 +41,8 @@ func TestK9sValidateBlank(t *testing.T) {
c.Validate(ksMock)
assert.Equal(t, 2, c.RefreshRate)
assert.Equal(t, 200, c.LogBufferSize)
assert.Equal(t, 1000, c.LogBufferSize)
assert.Equal(t, 200, c.LogRequestSize)
assert.Equal(t, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster)
assert.Equal(t, 1, len(c.Clusters))

View File

@ -1,6 +1,7 @@
k9s:
refreshRate: 2
logBufferSize: 200
logRequestSize: 200
currentContext: minikube
currentCluster: minikube
clusters:
@ -8,21 +9,21 @@ k9s:
namespace:
active: kube-system
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: ctx
fred:
namespace:
active: default
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: po
active: po

41
internal/k8s/access.go Normal file
View File

@ -0,0 +1,41 @@
package k8s
import (
"strings"
"github.com/rs/zerolog/log"
authorizationv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
)
// CanIAccess checks if user has access to a certain resource.
func CanIAccess(ns, verb, name, resURL string) bool {
_, gr := schema.ParseResourceArg(strings.ToLower(resURL))
sar := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: ns,
Verb: verb,
Group: gr.Group,
Resource: gr.Resource,
Subresource: "",
Name: name,
},
},
}
auth, err := kubernetes.NewForConfig(conn.restConfigOrDie())
if err != nil {
log.Warn().Msgf("%s", err)
return false
}
response, err := auth.AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
if err != nil {
log.Warn().Msgf("%s", err)
return false
}
return response.Status.Allowed
}

View File

@ -1,5 +1,7 @@
package k8s
import "github.com/rs/zerolog/log"
// Cluster represents a Kubernetes cluster.
type Cluster struct{}
@ -12,6 +14,7 @@ func NewCluster() *Cluster {
func (c *Cluster) Version() (string, error) {
rev, err := conn.dialOrDie().Discovery().ServerVersion()
if err != nil {
log.Warn().Msgf("%s", err)
return "", err
}
return rev.GitVersion, nil
@ -21,6 +24,7 @@ func (c *Cluster) Version() (string, error) {
func (c *Cluster) ContextName() string {
ctx, err := conn.config.CurrentContextName()
if err != nil {
log.Warn().Msgf("%s", err)
return "N/A"
}
return ctx
@ -30,6 +34,7 @@ func (c *Cluster) ContextName() string {
func (c *Cluster) ClusterName() string {
ctx, err := conn.config.CurrentClusterName()
if err != nil {
log.Warn().Msgf("%s", err)
return "N/A"
}
return ctx
@ -39,6 +44,7 @@ func (c *Cluster) ClusterName() string {
func (c *Cluster) UserName() string {
usr, err := conn.config.CurrentUserName()
if err != nil {
log.Warn().Msgf("%s", err)
return "N/A"
}
return usr

View File

@ -19,9 +19,9 @@ type NamedContext struct {
Context *api.Context
}
// MustCurrentClusterName return the active cluster name.
func (c *NamedContext) MustCurrentClusterName() string {
cl, err := conn.config.CurrentClusterName()
// MustCurrentContextName return the active context name.
func (c *NamedContext) MustCurrentContextName() string {
cl, err := conn.config.CurrentContextName()
if err != nil {
panic(err)
}

View File

@ -2,9 +2,16 @@ package k8s
import (
"fmt"
"os/user"
"path/filepath"
"regexp"
"strings"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/restmapper"
)
// RestMapping holds k8s resource mapping
@ -15,13 +22,92 @@ var RestMapping = &RestMapper{}
type RestMapper struct{}
// Find a mapping given a resource name.
func (*RestMapper) Find(res string) (*meta.RESTMapping, error) {
func (*RestMapper) Find1(res string) (*meta.RESTMapping, error) {
if m, ok := resMap[res]; ok {
return m, nil
}
return nil, fmt.Errorf("no mapping for resource %s", res)
}
func (*RestMapper) ToRESTMapper() (meta.RESTMapper, error) {
rc := conn.restConfigOrDie()
httpCacheDir := filepath.Join(mustHomeDir(), ".kube", "http-cache")
discCacheDir := filepath.Join(mustHomeDir(), ".kube", "cache", "discovery", toHostDir(rc.Host))
disc, err := discovery.NewCachedDiscoveryClientForConfig(rc, discCacheDir, httpCacheDir, 10*time.Minute)
if err != nil {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(disc)
expander := restmapper.NewShortcutExpander(mapper, disc)
return expander, nil
}
var toFileName = regexp.MustCompile(`[^(\w/\.)]`)
func toHostDir(host string) string {
h := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1)
// now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived
return toFileName.ReplaceAllString(h, "_")
}
func mustHomeDir() string {
usr, err := user.Current()
if err != nil {
panic(err)
}
return usr.HomeDir
}
// ResourceFor produces a rest mapping from a given resource.
// Support full res name ie deployment.v1.apps.
func (r *RestMapper) ResourceFor(resourceArg string) (*meta.RESTMapping, error) {
res, err := r.resourceFor(resourceArg)
if err != nil {
return nil, err
}
return r.toRESTMapping(res, resourceArg), nil
}
func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResource, error) {
if resourceArg == "*" {
return schema.GroupVersionResource{Resource: resourceArg}, nil
}
var (
gvr schema.GroupVersionResource
err error
)
mapper, err := r.ToRESTMapper()
if err != nil {
return gvr, err
}
fullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg))
if fullGVR != nil {
return mapper.ResourceFor(*fullGVR)
}
gvr, err = mapper.ResourceFor(gr.WithVersion(""))
if err != nil {
if len(gr.Group) == 0 {
return gvr, fmt.Errorf("the server doesn't have a resource type '%s'", gr.Resource)
}
return gvr, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gr.Resource, gr.Group)
}
return gvr, nil
}
func (*RestMapper) toRESTMapping(gvr schema.GroupVersionResource, res string) *meta.RESTMapping {
return &meta.RESTMapping{
Resource: gvr,
GroupVersionKind: schema.GroupVersionKind{Group: gvr.Group, Version: gvr.Version, Kind: res},
Scope: RestMapping,
}
}
// Name protocol returns rest scope name.
func (*RestMapper) Name() meta.RESTScopeName {
return meta.RESTScopeNameNamespace
@ -155,28 +241,3 @@ var resMap = map[string]*meta.RESTMapping{
Scope: RestMapping,
},
}
// {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"}: {},
// {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequestList"}: {},
// {Group: "kubeadm.k8s.io", Version: "v1alpha1", Kind: "MasterConfiguration"}: {},
// {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {},
// {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {},
// {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}: {},
// {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicyList"}: {},
// {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {},
// {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {},
// {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPreset"}: {},
// {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPresetList"}: {},
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {},
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {},
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {},
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfigurationList"}: {},
// {Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSink"}: {},
// {Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSinkList"}: {},
// {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}: {},
// {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicyList"}: {},
// {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClass"}: {},
// {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClassList"}: {},
// {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}: {},
// {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}: {},
// {Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}: {},

View File

@ -5,7 +5,8 @@ import (
"math"
"path"
"k8s.io/api/core/v1"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
metricsapi "k8s.io/metrics/pkg/apis/metrics"
@ -37,8 +38,10 @@ func (m *MetricsServer) NodeMetrics() (Metric, error) {
opts := metav1.ListOptions{}
nn, err := conn.dialOrDie().CoreV1().Nodes().List(opts)
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
nods := make([]string, len(nn.Items))
var maxCPU, maxMem float64
for i, n := range nn.Items {
@ -51,6 +54,7 @@ func (m *MetricsServer) NodeMetrics() (Metric, error) {
mm, err := m.getNodeMetrics()
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
@ -67,6 +71,7 @@ func (m *MetricsServer) NodeMetrics() (Metric, error) {
CPU: fmt.Sprintf("%0.f%%", math.Round((cpu/maxCPU)*100)),
Mem: fmt.Sprintf("%0.f%%", math.Round((mem/maxMem)*100)),
}
return mx, nil
}
@ -76,6 +81,7 @@ func (m *MetricsServer) PodMetrics() (map[string]Metric, error) {
mm, err := m.getPodMetrics()
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
@ -88,6 +94,7 @@ func (m *MetricsServer) PodMetrics() (map[string]Metric, error) {
pa := path.Join(m.Namespace, m.Name)
mx[pa] = Metric{CPU: fmt.Sprintf("%dm", cpu), Mem: fmt.Sprintf("%dMi", mem)}
}
return mx, nil
}
@ -97,6 +104,7 @@ func (m *MetricsServer) PerNodeMetrics(nn []v1.Node) (map[string]Metric, error)
mm, err := m.getNodeMetrics()
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
@ -117,6 +125,7 @@ func (m *MetricsServer) PerNodeMetrics(nn []v1.Node) (map[string]Metric, error)
AvailMem: fmt.Sprintf("%dMi", amem.Value()/(1024*1024)),
}
}
return mx, nil
}
@ -124,70 +133,86 @@ func (m *MetricsServer) getPodMetrics() (*metricsapi.PodMetricsList, error) {
if conn.hasMetricsServer() {
return m.podMetricsViaService()
}
selector := labels.Everything()
var mx *metricsapi.PodMetricsList
conn, err := conn.heapsterDial()
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
return conn.GetPodMetrics("", "", true, selector)
return conn.GetPodMetrics("", "", true, labels.Everything())
}
func (m *MetricsServer) getNodeMetrics() (*metricsapi.NodeMetricsList, error) {
if conn.hasMetricsServer() {
return m.nodeMetricsViaService()
}
selector := labels.Everything()
var mx *metricsapi.NodeMetricsList
conn, err := conn.heapsterDial()
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
return conn.GetNodeMetrics("", selector.String())
return conn.GetNodeMetrics("", labels.Everything().String())
}
func (*MetricsServer) nodeMetricsViaService() (*metricsapi.NodeMetricsList, error) {
var mx *metricsapi.NodeMetricsList
clt, err := conn.mxsDial()
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
selector := labels.Everything()
var versionedMetrics *metricsV1beta1api.NodeMetricsList
mc := clt.Metrics()
nm := mc.NodeMetricses()
versionedMetrics, err = nm.List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
log.Warn().Msgf("%s", err)
return nil, err
}
metrics := &metricsapi.NodeMetricsList{}
err = metricsV1beta1api.Convert_v1beta1_NodeMetricsList_To_metrics_NodeMetricsList(versionedMetrics, metrics, nil)
if err != nil {
log.Warn().Msgf("%s", err)
return nil, err
}
return metrics, nil
}
func (*MetricsServer) podMetricsViaService() (*metricsapi.PodMetricsList, error) {
var mx *metricsapi.PodMetricsList
clt, err := conn.mxsDial()
if err != nil {
log.Warn().Msgf("%s", err)
return mx, err
}
selector := labels.Everything()
var versionedMetrics *metricsV1beta1api.PodMetricsList
mc := clt.Metrics()
nm := mc.PodMetricses("")
versionedMetrics, err = nm.List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
log.Warn().Msgf("%s", err)
return nil, err
}
metrics := &metricsapi.PodMetricsList{}
err = metricsV1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, metrics, nil)
if err != nil {
log.Warn().Msgf("%s", err)
return nil, err
}
return metrics, nil
}

View File

@ -1,6 +1,7 @@
package k8s
import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -12,13 +13,14 @@ func NewNamespace() Res {
return &Namespace{}
}
// Get a namespace.
// Get a active namespace.
func (*Namespace) Get(_, n string) (interface{}, error) {
opts := metav1.GetOptions{}
return conn.dialOrDie().CoreV1().Namespaces().Get(n, opts)
}
// List all namespaces on the cluster.
// List all active namespaces on the cluster.
func (*Namespace) List(_ string) (Collection, error) {
opts := metav1.ListOptions{}
@ -29,7 +31,9 @@ func (*Namespace) List(_ string) (Collection, error) {
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
if r.Status.Phase == v1.NamespaceActive {
cc[i] = r
}
}
return cc, nil
@ -38,5 +42,6 @@ func (*Namespace) List(_ string) (Collection, error) {
// Delete a namespace.
func (*Namespace) Delete(_, n string) error {
opts := metav1.DeleteOptions{}
return conn.dialOrDie().CoreV1().Namespaces().Delete(n, &opts)
}

View File

@ -0,0 +1,25 @@
package printer
import (
"fmt"
)
// Defines basic ANSI colors.
const (
ColorBlack = iota + 30
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
ColorBold = 1
ColorDarkGray = 90
)
// Colorize a string based on given color.
func Colorize(s string, c int) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s)
}

View File

@ -70,12 +70,13 @@ func (b *Base) List(ns string) (Columnars, error) {
// Describe a given resource.
func (b *Base) Describe(kind, pa string) (string, error) {
ns, n := namespaced(pa)
mapping, err := k8s.RestMapping.Find(kind)
mapping, err := k8s.RestMapping.Find1(kind)
if err != nil {
return "", err
}
d, err := versioned.Describer(k8s.KubeConfig.Flags(), mapping)
if err != nil {
return "", err

View File

@ -79,7 +79,7 @@ func (r *Context) Fields(ns string) Row {
i := r.instance
name := i.Name
if i.MustCurrentClusterName() == name {
if i.MustCurrentContextName() == name {
name += "*"
}

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/apimachinery/pkg/watch"
@ -82,12 +83,8 @@ func Pad(s string, l int) string {
}
// Truncate a string to the given l and suffix ellipsis if needed.
func Truncate(s string, l int) string {
if len(s) > l {
fmat := "%." + strconv.Itoa(l) + "s%s"
return fmt.Sprintf(fmat, s, string(tview.SemigraphicsHorizontalEllipsis))
}
return s
func Truncate(str string, width int) string {
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
}
func mapToStr(m map[string]string) (s string) {

View File

@ -82,8 +82,8 @@ func TestTruncate(t *testing.T) {
l int
e string
}{
{"fred", 2, "fr…"},
{"fred", 1, "f…"},
{"fred", 3, "fr…"},
{"fred", 2, "f…"},
{"fred", 10, "fred"},
}

View File

@ -19,6 +19,7 @@ const (
type aliasView struct {
*tableView
current igniter
cancel context.CancelFunc
}
@ -32,9 +33,9 @@ func newAliasView(app *appView) *aliasView {
v.sortFn = v.sorterFn
v.currentNS = ""
}
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.gotoCmd)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd)
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
ctx, cancel := context.WithCancel(context.TODO())
v.cancel = cancel
@ -50,6 +51,7 @@ func newAliasView(app *appView) *aliasView {
}
}
}(ctx)
return &v
}
@ -73,6 +75,7 @@ func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
v.cmdBuff.reset()
return nil
}
return v.backCmd(evt)
}
@ -85,6 +88,7 @@ func (v *aliasView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() {
return v.filterCmd(evt)
}
return evt
}
@ -92,19 +96,22 @@ func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cancel != nil {
v.cancel()
}
if v.cmdBuff.isActive() {
v.cmdBuff.reset()
} else {
v.app.inject(v.current)
}
return nil
}
func (v *aliasView) runCmd(evt *tcell.EventKey) *tcell.EventKey {
r, _ := v.GetSelection()
if r > 0 {
v.app.command.run(strings.TrimSpace(v.GetCell(r, 0).Text))
v.app.gotoResource(strings.TrimSpace(v.GetCell(r, 0).Text), true)
}
return nil
}
@ -133,6 +140,7 @@ func (v *aliasView) hydrate() resource.TableData {
Deltas: fields,
}
}
return data
}

View File

@ -34,20 +34,21 @@ type (
appView struct {
*tview.Application
version string
pages *tview.Pages
content *tview.Pages
flashView *flashView
menuView *menuView
infoView *infoView
command *command
focusGroup []tview.Primitive
focusCurrent int
focusChanged focusHandler
cancel context.CancelFunc
cmdBuff *cmdBuff
cmdView *cmdView
actions keyActions
version string
pages *tview.Pages
content *tview.Pages
flashView *flashView
crumbsView *crumbsView
menuView *menuView
clusterInfoView *clusterInfoView
command *command
focusGroup []tview.Primitive
focusCurrent int
focusChanged focusHandler
cancel context.CancelFunc
cmdBuff *cmdBuff
cmdView *cmdView
actions keyActions
}
)
@ -68,32 +69,35 @@ func NewApp() *appView {
v.cmdView = newCmdView('🐶')
v.command = newCommand(&v)
v.flashView = newFlashView(v.Application, "Initializing...")
v.infoView = newInfoView(&v)
v.crumbsView = newCrumbsView(v.Application)
v.clusterInfoView = newInfoView(&v)
v.focusChanged = v.changedFocus
v.SetInputCapture(v.keyboard)
}
v.actions[KeyColon] = newKeyAction("Cmd", v.activateCmd)
v.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", v.redrawCmd)
v.actions[KeyQ] = newKeyAction("Quit", v.quitCmd)
v.actions[KeyHelp] = newKeyAction("Help", v.helpCmd)
v.actions[tcell.KeyEscape] = newKeyAction("Exit Cmd", v.deactivateCmd)
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Goto", v.eraseCmd)
v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd)
v.actions[KeyColon] = newKeyAction("Cmd", v.activateCmd, false)
v.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", v.redrawCmd, false)
v.actions[KeyQ] = newKeyAction("Quit", v.quitCmd, false)
v.actions[KeyHelp] = newKeyAction("Help", v.helpCmd, false)
v.actions[KeyA] = newKeyAction("Aliases", v.aliasCmd, true)
v.actions[tcell.KeyEscape] = newKeyAction("Exit Cmd", v.deactivateCmd, false)
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, false)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd, false)
return &v
}
func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags) {
a.version = v
a.infoView.init()
a.clusterInfoView.init()
a.cmdBuff.addListener(a.cmdView)
header := tview.NewFlex()
{
header.SetDirection(tview.FlexColumn)
header.AddItem(a.infoView, 55, 1, false)
header.AddItem(a.clusterInfoView, 55, 1, false)
header.AddItem(a.menuView, 0, 1, false)
header.AddItem(logoView(), 26, 1, false)
}
@ -104,11 +108,12 @@ func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags)
main.AddItem(header, 7, 1, false)
main.AddItem(a.cmdView, 1, 1, false)
main.AddItem(a.content, 0, 10, true)
main.AddItem(a.flashView, 2, 1, false)
main.AddItem(a.crumbsView, 2, 1, false)
main.AddItem(a.flashView, 1, 1, false)
}
a.pages.AddPage("main", main, true, false)
a.pages.AddPage("splash", NewSplash(a.version), true, true)
a.pages.AddPage("splash", newSplash(a.version), true, true)
a.SetRoot(a.pages, true)
}
@ -167,9 +172,18 @@ func (a *appView) deactivateCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
if top, ok := a.command.previousCmd(); ok {
log.Debug().Msgf("Previous command %s", top)
a.gotoResource(top, false)
return nil
}
return evt
}
func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdBuff.isActive() && !a.cmdBuff.empty() {
a.command.run(a.cmdBuff.String())
a.gotoResource(a.cmdBuff.String(), true)
a.cmdBuff.reset()
return nil
}
@ -197,6 +211,14 @@ func (a *appView) quitCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdView.inCmdMode() {
return evt
}
a.inject(newHelpView(a))
return nil
}
func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdView.inCmdMode() {
return evt
}
@ -212,6 +234,14 @@ func (a *appView) puntCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
func (a *appView) gotoResource(res string, record bool) bool {
valid := a.command.run(res)
if valid && record {
a.command.pushCmd(res)
}
return valid
}
func (a *appView) showPage(p string) {
a.pages.SwitchToPage(p)
}
@ -240,7 +270,7 @@ func (a *appView) cmdMode() bool {
}
func (a *appView) refresh() {
a.infoView.refresh()
a.clusterInfoView.refresh()
}
func (a *appView) flash(level flashLevel, m ...string) {
@ -248,7 +278,7 @@ func (a *appView) flash(level flashLevel, m ...string) {
}
func (a *appView) setHints(h hints) {
a.menuView.setMenu(h)
a.menuView.populateMenu(h)
}
func logoView() tview.Primitive {

View File

@ -7,17 +7,17 @@ import (
"github.com/rs/zerolog/log"
)
type infoView struct {
type clusterInfoView struct {
*tview.Table
app *appView
}
func newInfoView(app *appView) *infoView {
return &infoView{app: app, Table: tview.NewTable()}
func newInfoView(app *appView) *clusterInfoView {
return &clusterInfoView{app: app, Table: tview.NewTable()}
}
func (v *infoView) init() {
func (v *clusterInfoView) init() {
cluster := resource.NewCluster()
var row int
@ -49,20 +49,22 @@ func (v *infoView) init() {
v.refresh()
}
func (*infoView) sectionCell(t string) *tview.TableCell {
func (*clusterInfoView) sectionCell(t string) *tview.TableCell {
c := tview.NewTableCell(t + ":")
c.SetAlign(tview.AlignLeft)
return c
}
func (*infoView) infoCell(t string) *tview.TableCell {
func (*clusterInfoView) infoCell(t string) *tview.TableCell {
c := tview.NewTableCell(t)
c.SetExpansion(2)
c.SetTextColor(tcell.ColorOrange)
return c
}
func (v *infoView) refresh() {
func (v *clusterInfoView) refresh() {
var row int
cluster := resource.NewCluster()
@ -77,7 +79,7 @@ func (v *infoView) refresh() {
mx, err := cluster.Metrics()
if err != nil {
log.Error().Err(err)
log.Warn().Msgf("%s", err)
return
}
c := v.GetCell(row, 1)

View File

@ -0,0 +1,46 @@
package views
import "github.com/rs/zerolog/log"
const maxStackSize = 10
type cmdStack struct {
index int
stack []string
}
func newCmdStack() *cmdStack {
return &cmdStack{stack: make([]string, 0, maxStackSize)}
}
func (s *cmdStack) push(cmd string) {
if len(s.stack) == maxStackSize {
s.stack = s.stack[1 : len(s.stack)-1]
}
s.stack = append(s.stack, cmd)
log.Info().Msgf("Pushed %s %v", cmd, s.stack)
}
func (s *cmdStack) pop() (string, bool) {
if s.empty() {
return "", false
}
log.Info().Msgf("Before Pop %v", s.stack)
top := s.stack[len(s.stack)-1]
s.stack = s.stack[:len(s.stack)-1]
log.Info().Msgf("After Pop %v", s.stack)
return top, true
}
func (s *cmdStack) top() (string, bool) {
if s.empty() {
return "", false
}
log.Info().Msgf("Top %v -- %s", s.stack, s.stack[len(s.stack)-1])
return s.stack[len(s.stack)-1], true
}
func (s *cmdStack) empty() bool {
return len(s.stack) == 0
}

View File

@ -0,0 +1,76 @@
package views
import (
"fmt"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)
func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel)
}
func TestCmdStackPushMax(t *testing.T) {
s := newCmdStack()
for i := 0; i < 20; i++ {
s.push(fmt.Sprintf("cmd_%d", i))
}
top, ok := s.top()
assert.True(t, ok)
assert.Equal(t, "cmd_19", top)
}
func TestCmdStackPop(t *testing.T) {
type expect struct {
val string
ok bool
}
uu := []struct {
cmds []string
popCount int
e expect
}{
{[]string{}, 2, expect{"", false}},
{[]string{"a", "b", "c"}, 2, expect{"a", true}},
{[]string{"a", "b", "c"}, 1, expect{"b", true}},
}
for _, u := range uu {
s := newCmdStack()
for _, v := range u.cmds {
s.push(v)
}
for i := 0; i < u.popCount; i++ {
s.pop()
}
top, ok := s.pop()
assert.Equal(t, u.e.ok, ok)
assert.Equal(t, u.e.val, top)
}
}
func TestCmdStackEmpty(t *testing.T) {
uu := []struct {
cmds []string
popCount int
e bool
}{
{[]string{}, 0, true},
{[]string{"a", "b", "c"}, 0, false},
{[]string{"a", "b", "c"}, 3, true},
}
for _, u := range uu {
s := newCmdStack()
for _, v := range u.cmds {
s.push(v)
}
for i := 0; i < u.popCount; i++ {
s.pop()
}
assert.Equal(t, u.e, s.empty())
}
}

View File

@ -8,43 +8,56 @@ import (
)
type command struct {
app *appView
app *appView
history *cmdStack
}
func newCommand(app *appView) *command {
return &command{app: app}
return &command{app: app, history: newCmdStack()}
}
func (c *command) pushCmd(cmd string) {
c.history.push(cmd)
c.app.crumbsView.update(c.history.stack)
}
func (c *command) previousCmd() (string, bool) {
c.history.pop()
c.app.crumbsView.update(c.history.stack)
return c.history.top()
}
// DefaultCmd reset default command ie show pods.
func (c *command) defaultCmd() {
c.pushCmd(config.Root.ActiveView())
c.run(config.Root.ActiveView())
}
// Helpers...
// Exec the command by showing associated display.
func (c *command) run(cmd string) {
func (c *command) run(cmd string) bool {
var v igniter
switch cmd {
case "q", "quit":
c.app.Stop()
return
return true
case "?", "help", "alias":
c.app.inject(newAliasView(c.app))
return
return true
default:
if res, ok := cmdMap[cmd]; ok {
if res, ok := resourceViews()[cmd]; ok {
v = res.viewFn(res.title, c.app, res.listFn(resource.DefaultNamespace), res.colorerFn)
c.app.flash(flashInfo, "Viewing all "+res.title+"...")
c.app.flash(flashInfo, fmt.Sprintf("Viewing %s in namespace %s...", res.title, config.Root.ActiveNamespace()))
c.exec(cmd, v)
return
return true
}
}
res, ok := getCRDS()[cmd]
res, ok := allCRDs()[cmd]
if !ok {
c.app.flash(flashWarn, fmt.Sprintf("Huh? `%s` command not found", cmd))
return
return false
}
n := res.Plural
@ -58,6 +71,7 @@ func (c *command) run(cmd string) {
defaultColorer,
)
c.exec(cmd, v)
return true
}
func (c *command) exec(cmd string, v igniter) {

View File

@ -14,19 +14,33 @@ type contextView struct {
func newContextView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
v := contextView{newResourceView(t, app, list, c).(*resourceView)}
v.extraActionsFn = v.extraActions
v.switchPage("ctx")
{
v.extraActionsFn = v.extraActions
v.switchPage("ctx")
}
return &v
}
func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey {
func (v *contextView) extraActions(aa keyActions) {
aa[tcell.KeyEnter] = newKeyAction("Switch", v.useCmd, true)
}
func (v *contextView) useCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() {
return evt
}
if err := v.useContext(v.selectedItem); err != nil {
v.app.flash(flashWarn, err.Error())
return evt
}
ctx := strings.TrimSpace(v.selectedItem)
v.app.gotoResource("po", true)
return nil
}
func (v *contextView) useContext(name string) error {
ctx := strings.TrimSpace(name)
if strings.HasSuffix(ctx, "*") {
ctx = strings.TrimRight(ctx, "*")
}
@ -34,15 +48,12 @@ func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey {
ctx = strings.TrimRight(ctx, "(𝜟)")
}
err := v.list.Resource().(*resource.Context).Switch(ctx)
if err != nil {
v.app.flash(flashWarn, err.Error())
return evt
if err := v.list.Resource().(*resource.Context).Switch(ctx); err != nil {
return err
}
config.Root.Reset()
config.Root.Save()
v.app.flash(flashInfo, "Switching context to", ctx)
v.refresh()
if tv, ok := v.GetPrimitive("ctx").(*tableView); ok {
@ -50,7 +61,3 @@ func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey {
}
return nil
}
func (v *contextView) extraActions(aa keyActions) {
aa[KeyU] = newKeyAction("Use", v.useContext)
}

View File

@ -34,5 +34,5 @@ func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey {
}
func (v *cronJobView) extraActions(aa keyActions) {
aa[tcell.KeyCtrlT] = newKeyAction("Trigger", v.trigger)
aa[tcell.KeyCtrlT] = newKeyAction("Trigger", v.trigger, true)
}

36
internal/views/crumbs.go Normal file
View File

@ -0,0 +1,36 @@
package views
import (
"fmt"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
type crumbsView struct {
*tview.TextView
app *tview.Application
}
func newCrumbsView(app *tview.Application) *crumbsView {
v := crumbsView{app: app, TextView: tview.NewTextView()}
{
v.SetTextColor(tcell.ColorAqua)
v.SetTextAlign(tview.AlignLeft)
v.SetBorderPadding(0, 0, 1, 1)
v.SetDynamicColors(true)
}
return &v
}
func (v *crumbsView) update(crumbs []string) {
v.Clear()
last, bgColor := len(crumbs)-1, "aqua"
for i, c := range crumbs {
if i == last {
bgColor = "orange"
}
fmt.Fprintf(v, "[black:%s:b] <%s> [-:-:-] ", bgColor, c)
}
}

View File

@ -53,12 +53,13 @@ func newDetailsView(app *appView, backFn actionHandler) *detailsView {
})
}
v.actions[KeySlash] = newKeyAction("Search", v.activateCmd)
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.searchCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.backCmd)
v.actions[tcell.KeyTab] = newKeyAction("Next", v.nextCmd)
v.actions[tcell.KeyBacktab] = newKeyAction("Previous", v.prevCmd)
// v.actions[KeySlash] = newKeyAction("Search", v.activateCmd)
// v.actions[tcell.KeyEnter] = newKeyAction("Search", v.searchCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, true)
v.actions[tcell.KeyTab] = newKeyAction("Next Match", v.nextCmd, false)
v.actions[tcell.KeyBacktab] = newKeyAction("Previous Match", v.prevCmd, false)
return &v
}

View File

@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
@ -34,12 +35,12 @@ type (
)
func newFlashView(app *tview.Application, m string) *flashView {
var f flashView
f := flashView{app: app, TextView: tview.NewTextView()}
{
f = flashView{app: app, TextView: tview.NewTextView()}
f.SetTextColor(tcell.ColorAqua)
f.SetTextAlign(tview.AlignLeft)
f.SetBorderPadding(0, 0, 1, 1)
f.SetText(m)
}
return &f
}
@ -48,14 +49,17 @@ func (f *flashView) setMessage(level flashLevel, msg ...string) {
if f.cancel != nil {
f.cancel()
}
var ctx context.Context
{
ctx, f.cancel = context.WithTimeout(context.TODO(), flashDelay*time.Second)
go func(ctx context.Context) {
_, _, width, _ := f.GetRect()
if width <= 15 {
width = 100
}
m := strings.Join(msg, " ")
f.SetTextColor(flashColor(level))
f.SetText(flashEmoji(level) + " " + m)
f.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3))
f.app.Draw()
for {
select {

120
internal/views/help.go Normal file
View File

@ -0,0 +1,120 @@
package views
import (
"context"
"fmt"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
const (
helpTitle = "Help"
helpTitleFmt = " [aqua::b]%s "
)
type helpView struct {
*tview.TextView
app *appView
current igniter
actions keyActions
}
func newHelpView(app *appView) *helpView {
v := helpView{TextView: tview.NewTextView(), app: app, actions: make(keyActions)}
{
v.SetTextColor(tcell.ColorAqua)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
v.SetDynamicColors(true)
v.SetInputCapture(v.keyboard)
v.current = app.content.GetPrimitive("main").(igniter)
}
v.actions[tcell.KeyEsc] = newKeyAction("Back", v.backCmd, true)
v.actions[tcell.KeyEnter] = newKeyAction("Back", v.backCmd, false)
return &v
}
func (v *helpView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key()
if key == tcell.KeyRune {
key = tcell.Key(evt.Rune())
}
if a, ok := v.actions[key]; ok {
log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key])
return a.action(evt)
}
return evt
}
func (v *helpView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.app.inject(v.current)
return nil
}
func (v *helpView) init(_ context.Context, _ string) {
v.resetTitle()
type helpItem struct {
key, description string
}
general := []helpItem{
{":<cmd>", "Command mode"},
{"/<term>", "Filter mode"},
{"esc", "Clear filter"},
{"tab", "Next term match"},
{"backtab", "Previous term match"},
{"Ctrl-r", "Refresh"},
{"p", "Previous resource view"},
{"q", "Quit"},
}
fmt.Fprintf(v, "🏠 [aqua::b]%s\n", "General")
for _, h := range general {
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
}
navigation := []helpItem{
{"g", "Goto Top"},
{"G", "Goto Bottom"},
{"b", "Page Down"},
{"f", "Page Up"},
{"l", "Left"},
{"h", "Right"},
{"k", "Up"},
{"j", "Down"},
}
fmt.Fprintln(v)
fmt.Fprintf(v, "🖲 [aqua::b]%s\n", "View Navigation")
for _, h := range navigation {
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
}
views := []helpItem{
{"?", "Help"},
{"a", "Aliases view"},
}
fmt.Fprintln(v)
fmt.Fprintf(v, "️️⁉️ [aqua::b]%s\n", "Help")
for _, h := range views {
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
}
v.app.setHints(v.hints())
}
func (v *helpView) hints() hints {
return v.actions.toHints()
}
func (v *helpView) getTitle() string {
return helpTitle
}
func (v *helpView) resetTitle() {
v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))
}

View File

@ -64,5 +64,5 @@ func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey {
}
func (v *jobView) extraActions(aa keyActions) {
aa[KeyL] = newKeyAction("Logs", v.logs)
aa[KeyL] = newKeyAction("Logs", v.logs, true)
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"io"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tview"
)
@ -20,6 +21,7 @@ func newLogView(title string, parent loggable) *logView {
v.SetDynamicColors(true)
v.SetWrap(true)
v.setTitle(parent.getSelection())
v.SetMaxBuffer(config.Root.K9s.LogBufferSize)
}
v.ansiWriter = tview.ANSIWriter(v)
return &v

View File

@ -35,12 +35,12 @@ func newLogsView(parent loggable) *logsView {
containers: []string{},
}
v.setActions(keyActions{
tcell.KeyEscape: {description: "Back", action: v.back},
KeyC: {description: "Clear", action: v.clearLogs},
KeyG: {description: "Top", action: v.top},
KeyShiftG: {description: "Bottom", action: v.bottom},
KeyF: {description: "Up", action: v.pageUp},
KeyB: {description: "Down", action: v.pageDown},
tcell.KeyEscape: {description: "Back", action: v.back, visible: true},
KeyC: {description: "Clear", action: v.clearLogs, visible: true},
KeyG: {description: "Top", action: v.top, visible: false},
KeyShiftG: {description: "Bottom", action: v.bottom, visible: false},
KeyF: {description: "Up", action: v.pageUp, visible: false},
KeyB: {description: "Down", action: v.pageDown, visible: false},
})
v.SetInputCapture(v.keyboard)
@ -91,7 +91,7 @@ func (v *logsView) setActions(aa keyActions) {
func (v *logsView) hints() hints {
if len(v.containers) > 1 {
for i, c := range v.containers {
v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil)
v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil, true)
}
}
return v.actions.toHints()
@ -163,7 +163,7 @@ func (v *logsView) doLoad(path, co string) error {
if !ok {
return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource)
}
maxBuff := int64(config.Root.K9s.LogBufferSize)
maxBuff := int64(config.Root.K9s.LogRequestSize)
cancelFn, err := res.Logs(c, ns, po, co, maxBuff, false)
if err != nil {
cancelFn()
@ -200,14 +200,18 @@ func (v *logsView) bottom(*tcell.EventKey) *tcell.EventKey {
func (v *logsView) pageUp(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil {
p.Item.(*logView).PageUp()
if p.Item.(*logView).PageUp() {
v.parent.appView().flash(flashInfo, "Reached Top ...")
}
}
return nil
}
func (v *logsView) pageDown(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil {
p.Item.(*logView).PageDown()
if p.Item.(*logView).PageDown() {
v.parent.appView().flash(flashInfo, "Reached Bottom ...")
}
}
return nil
}

View File

@ -14,17 +14,16 @@ import (
)
const (
menuSepFmt = " [dodgerblue::b]%-9s [white::d]%s "
menuSepFmt = " [dodgerblue::b]%-%ds [white::d]%s "
menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s "
maxRows = 6
colLen = 20
maxRows = 7
)
var menuRX = regexp.MustCompile(`\d`)
type (
hint struct {
mnemonic, display string
mnemonic, description string
}
hints []hint
@ -53,7 +52,7 @@ func (h hints) Less(i, j int) bool {
if err1 != nil && err2 == nil {
return false
}
return strings.Compare(h[i].mnemonic, h[j].mnemonic) < 0
return strings.Compare(h[i].description, h[j].description) < 0
}
// -----------------------------------------------------------------------------
@ -63,12 +62,13 @@ type (
keyAction struct {
description string
action actionHandler
visible bool
}
keyActions map[tcell.Key]keyAction
)
func newKeyAction(d string, a actionHandler) keyAction {
return keyAction{description: d, action: a}
func newKeyAction(d string, a actionHandler, display bool) keyAction {
return keyAction{description: d, action: a, visible: display}
}
func newMenuView() *menuView {
@ -76,72 +76,21 @@ func newMenuView() *menuView {
return &v
}
// -----------------------------------------------------------------------------
type menuView struct {
*tview.Table
}
func (v *menuView) setMenu(hh hints) {
v.Clear()
sort.Sort(hh)
var row, col int
firstNS, firstCmd := true, true
for _, h := range hh {
isDigit := menuRX.MatchString(h.mnemonic)
if isDigit && firstNS {
row, col, firstNS = 0, 2, false
}
if !isDigit && firstCmd {
row, col, firstCmd = 0, 0, false
}
c := tview.NewTableCell(v.item(h))
v.SetCell(row, col, c)
row++
if row > maxRows {
col++
row = 0
}
}
}
func (*menuView) toMnemonic(s string) string {
return "<" + strings.ToLower(s) + ">"
}
func (v *menuView) item(h hint) string {
i, err := strconv.Atoi(h.mnemonic)
if err == nil {
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14))
}
return fmt.Sprintf(menuSepFmt, v.toMnemonic(h.mnemonic), h.display)
}
func (a keyActions) skipKey(k tcell.Key) bool {
switch k {
case tcell.KeyBackspace2:
fallthrough
case tcell.KeyEnter:
return true
}
return false
}
func (a keyActions) toHints() hints {
kk := make([]int, 0, len(a))
for k := range a {
if !a.skipKey(k) {
for k, v := range a {
if v.visible {
kk = append(kk, int(k))
}
}
sort.Ints(kk)
hh := make(hints, 0, len(a))
hh := make(hints, 0, len(kk))
for _, k := range kk {
if name, ok := tcell.KeyNames[tcell.Key(k)]; ok {
hh = append(hh, hint{
mnemonic: name,
display: a[tcell.Key(k)].description})
mnemonic: name,
description: a[tcell.Key(k)].description})
} else {
log.Error().Msgf("Unable to local KeyName for %#v", k)
}
@ -149,6 +98,85 @@ func (a keyActions) toHints() hints {
return hh
}
// -----------------------------------------------------------------------------
type menuView struct {
*tview.Table
}
func (v *menuView) populateMenu(hh hints) {
v.Clear()
sort.Sort(hh)
t := v.buildMenuTable(hh)
for row := 0; row < len(t); row++ {
for col := 0; col < len(t[row]); col++ {
if len(t[row][col]) == 0 {
continue
}
c := tview.NewTableCell(t[row][col])
v.SetCell(row, col, c)
}
}
}
func (v *menuView) buildMenuTable(hh hints) [][]string {
table := make([][]hint, maxRows+1)
colCount := (len(hh) / maxRows) + 1
for row := 0; row < maxRows; row++ {
table[row] = make([]hint, colCount)
}
var row, col int
firstNS, firstCmd := true, true
maxKeys := make([]int, colCount)
for _, h := range hh {
isDigit := menuRX.MatchString(h.mnemonic)
if isDigit && firstNS {
row, col, firstNS = 0, col+1, false
}
if !isDigit && firstCmd {
row, col, firstCmd = 0, 0, false
}
if maxKeys[col] < len(h.mnemonic) {
maxKeys[col] = len(h.mnemonic)
}
table[row][col] = h
row++
if row >= maxRows {
col++
row = 0
}
}
strTable := make([][]string, maxRows+1)
for r := 0; r < len(table); r++ {
strTable[r] = make([]string, len(table[r]))
}
for row := range strTable {
for col := range strTable[row] {
strTable[row][col] = v.formatMenu(table[row][col], maxKeys[col])
}
}
return strTable
}
func (*menuView) toMnemonic(s string) string {
if len(s) == 0 {
return s
}
return "<" + strings.ToLower(s) + ">"
}
func (v *menuView) formatMenu(h hint, size int) string {
i, err := strconv.Atoi(h.mnemonic)
if err == nil {
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.description, 14))
}
menuFmt := " [dodgerblue::b]%-" + strconv.Itoa(size+2) + "s [white::d]%s "
return fmt.Sprintf(menuFmt, v.toMnemonic(h.mnemonic), h.description)
}
// -----------------------------------------------------------------------------
// Key mapping Constants

View File

@ -5,6 +5,7 @@ import (
"regexp"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
)
@ -31,21 +32,34 @@ func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) r
}
func (v *namespaceView) extraActions(aa keyActions) {
aa[KeyU] = newKeyAction("Use", v.useNamespace)
aa[tcell.KeyEnter] = newKeyAction("Switch", v.switchNsCmd, true)
aa[KeyU] = newKeyAction("Use", v.useNsCmd, true)
}
func (v *namespaceView) useNamespace(evt *tcell.EventKey) *tcell.EventKey {
func (v *namespaceView) switchNsCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() {
return evt
}
ns := v.getSelectedItem()
if err := config.Root.SetActiveNamespace(ns); err != nil {
v.useNamespace(v.getSelectedItem())
v.app.gotoResource("po", true)
return nil
}
func (v *namespaceView) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() {
return evt
}
v.useNamespace(v.getSelectedItem())
return nil
}
func (v *namespaceView) useNamespace(name string) {
if err := config.Root.SetActiveNamespace(name); err != nil {
v.app.flash(flashErr, err.Error())
} else {
v.app.flash(flashInfo, fmt.Sprintf("Namespace %s is now active!", ns))
v.app.flash(flashInfo, fmt.Sprintf("Namespace %s is now active!", name))
}
config.Root.Save()
return nil
}
func (v *namespaceView) getSelectedItem() string {
@ -58,10 +72,12 @@ func (*namespaceView) cleanser(s string) string {
func (v *namespaceView) decorate(data resource.TableData) resource.TableData {
if _, ok := data.Rows[resource.AllNamespaces]; !ok {
data.Rows[resource.AllNamespace] = &resource.RowEvent{
Action: resource.Unchanged,
Fields: resource.Row{resource.AllNamespace, "Active", "0"},
Deltas: resource.Row{"", "", ""},
if k8s.CanIAccess("", "list", "namespaces", "namespace.v1") {
data.Rows[resource.AllNamespace] = &resource.RowEvent{
Action: resource.Unchanged,
Fields: resource.Row{resource.AllNamespace, "Active", "0"},
Deltas: resource.Row{"", "", ""},
}
}
}
for k, v := range data.Rows {

View File

@ -141,8 +141,8 @@ func (v *podView) showLogs(path, co string, previous bool) {
}
func (v *podView) extraActions(aa keyActions) {
aa[KeyL] = newKeyAction("Logs", v.logsCmd)
aa[KeyS] = newKeyAction("Shell", v.shellCmd)
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
}
func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) {

View File

@ -20,203 +20,20 @@ type (
}
)
var cmdMap = map[string]resCmd{
"cm": {
title: "ConfigMaps",
api: "core",
viewFn: newResourceView,
listFn: resource.NewConfigMapList,
colorerFn: defaultColorer,
},
"cr": {
title: "ClusterRoles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleList,
colorerFn: defaultColorer,
},
"crb": {
title: "ClusterRoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleBindingList,
colorerFn: defaultColorer,
},
"crd": {
title: "CustomResourceDefinitions",
api: "apiextensions.k8s.io",
viewFn: newResourceView,
listFn: resource.NewCRDList,
colorerFn: defaultColorer,
},
"cron": {
title: "CronJobs",
api: "batch",
viewFn: newCronJobView,
listFn: resource.NewCronJobList,
colorerFn: defaultColorer,
},
"ctx": {
title: "Contexts",
api: "core",
viewFn: newContextView,
listFn: resource.NewContextList,
colorerFn: ctxColorer,
},
"ds": {
title: "DaemonSets",
api: "core",
viewFn: newResourceView,
listFn: resource.NewDaemonSetList,
colorerFn: dpColorer,
},
"dp": {
title: "Deployments",
api: "apps",
viewFn: newResourceView,
listFn: resource.NewDeploymentList,
colorerFn: dpColorer,
},
"ep": {
title: "EndPoints",
api: "core",
viewFn: newResourceView,
listFn: resource.NewEndpointsList,
colorerFn: defaultColorer,
},
"ev": {
title: "Events",
api: "core",
viewFn: newResourceView,
listFn: resource.NewEventList,
colorerFn: evColorer,
},
"hpa": {
title: "HorizontalPodAutoscalers",
api: "autoscaling",
viewFn: newResourceView,
listFn: resource.NewHPAList,
colorerFn: defaultColorer,
},
"ing": {
title: "Ingress",
api: "extensions",
viewFn: newResourceView,
listFn: resource.NewIngressList,
colorerFn: defaultColorer,
},
"job": {
title: "Jobs",
api: "batch",
viewFn: newJobView,
listFn: resource.NewJobList,
colorerFn: defaultColorer,
},
"no": {
title: "Nodes",
api: "core",
viewFn: newResourceView,
listFn: resource.NewNodeList,
colorerFn: nsColorer,
},
"ns": {
title: "Namespaces",
api: "core",
viewFn: newNamespaceView,
listFn: resource.NewNamespaceList,
colorerFn: nsColorer,
},
"po": {
title: "Pods",
api: "core",
viewFn: newPodView,
listFn: resource.NewPodList,
colorerFn: podColorer,
},
"pv": {
title: "PersistentVolumes",
api: "core",
viewFn: newResourceView,
listFn: resource.NewPVList,
colorerFn: pvColorer,
},
"pvc": {
title: "PersistentVolumeClaims",
api: "core",
viewFn: newResourceView,
listFn: resource.NewPVCList,
colorerFn: pvcColorer,
},
"rb": {
title: "RoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleBindingList,
colorerFn: defaultColorer,
},
"rc": {
title: "ReplicationControllers",
api: "v1",
viewFn: newResourceView,
listFn: resource.NewReplicationControllerList,
colorerFn: rsColorer,
},
"ro": {
title: "Roles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleList,
colorerFn: defaultColorer,
},
"rs": {
title: "ReplicaSets",
api: "apps",
viewFn: newResourceView,
listFn: resource.NewReplicaSetList,
colorerFn: rsColorer,
},
"sa": {
title: "ServiceAccounts",
api: "core",
viewFn: newResourceView,
listFn: resource.NewServiceAccountList,
colorerFn: defaultColorer,
},
"sec": {
title: "Secrets",
api: "core",
viewFn: newResourceView,
listFn: resource.NewSecretList,
colorerFn: defaultColorer,
},
"sts": {
title: "StatefulSets",
api: "apps",
viewFn: newResourceView,
listFn: resource.NewStatefulSetList,
colorerFn: stsColorer,
},
"svc": {
title: "Services",
api: "core",
viewFn: newResourceView,
listFn: resource.NewServiceList,
colorerFn: defaultColorer,
},
}
func helpCmds() map[string]resCmd {
cmdMap := resourceViews()
cmds := make(map[string]resCmd, len(cmdMap))
for k, v := range cmdMap {
cmds[k] = v
}
for k, v := range getCRDS() {
for k, v := range allCRDs() {
cmds[k] = resCmd{title: v.Kind, api: v.Group}
}
return cmds
}
func getCRDS() map[string]k8s.APIGroup {
func allCRDs() map[string]k8s.APIGroup {
m := map[string]k8s.APIGroup{}
list := resource.NewCRDList(resource.AllNamespaces)
ll, _ := list.Resource().List(resource.AllNamespaces)
@ -248,5 +65,193 @@ func getCRDS() map[string]k8s.APIGroup {
m[s] = grp
}
}
return m
}
func resourceViews() map[string]resCmd {
return map[string]resCmd{
"cm": {
title: "ConfigMaps",
api: "core",
viewFn: newResourceView,
listFn: resource.NewConfigMapList,
colorerFn: defaultColorer,
},
"cr": {
title: "ClusterRoles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleList,
colorerFn: defaultColorer,
},
"crb": {
title: "ClusterRoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleBindingList,
colorerFn: defaultColorer,
},
"crd": {
title: "CustomResourceDefinitions",
api: "apiextensions.k8s.io",
viewFn: newResourceView,
listFn: resource.NewCRDList,
colorerFn: defaultColorer,
},
"cron": {
title: "CronJobs",
api: "batch",
viewFn: newCronJobView,
listFn: resource.NewCronJobList,
colorerFn: defaultColorer,
},
"ctx": {
title: "Contexts",
api: "core",
viewFn: newContextView,
listFn: resource.NewContextList,
colorerFn: ctxColorer,
},
"ds": {
title: "DaemonSets",
api: "core",
viewFn: newResourceView,
listFn: resource.NewDaemonSetList,
colorerFn: dpColorer,
},
"dp": {
title: "Deployments",
api: "apps",
viewFn: newResourceView,
listFn: resource.NewDeploymentList,
colorerFn: dpColorer,
},
"ep": {
title: "EndPoints",
api: "core",
viewFn: newResourceView,
listFn: resource.NewEndpointsList,
colorerFn: defaultColorer,
},
"ev": {
title: "Events",
api: "core",
viewFn: newResourceView,
listFn: resource.NewEventList,
colorerFn: evColorer,
},
"hpa": {
title: "HorizontalPodAutoscalers",
api: "autoscaling",
viewFn: newResourceView,
listFn: resource.NewHPAList,
colorerFn: defaultColorer,
},
"ing": {
title: "Ingress",
api: "extensions",
viewFn: newResourceView,
listFn: resource.NewIngressList,
colorerFn: defaultColorer,
},
"job": {
title: "Jobs",
api: "batch",
viewFn: newJobView,
listFn: resource.NewJobList,
colorerFn: defaultColorer,
},
"no": {
title: "Nodes",
api: "core",
viewFn: newResourceView,
listFn: resource.NewNodeList,
colorerFn: nsColorer,
},
"ns": {
title: "Namespaces",
api: "core",
viewFn: newNamespaceView,
listFn: resource.NewNamespaceList,
colorerFn: nsColorer,
},
"po": {
title: "Pods",
api: "core",
viewFn: newPodView,
listFn: resource.NewPodList,
colorerFn: podColorer,
},
"pv": {
title: "PersistentVolumes",
api: "core",
viewFn: newResourceView,
listFn: resource.NewPVList,
colorerFn: pvColorer,
},
"pvc": {
title: "PersistentVolumeClaims",
api: "core",
viewFn: newResourceView,
listFn: resource.NewPVCList,
colorerFn: pvcColorer,
},
"rb": {
title: "RoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleBindingList,
colorerFn: defaultColorer,
},
"rc": {
title: "ReplicationControllers",
api: "v1",
viewFn: newResourceView,
listFn: resource.NewReplicationControllerList,
colorerFn: rsColorer,
},
"ro": {
title: "Roles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleList,
colorerFn: defaultColorer,
},
"rs": {
title: "ReplicaSets",
api: "apps",
viewFn: newResourceView,
listFn: resource.NewReplicaSetList,
colorerFn: rsColorer,
},
"sa": {
title: "ServiceAccounts",
api: "core",
viewFn: newResourceView,
listFn: resource.NewServiceAccountList,
colorerFn: defaultColorer,
},
"sec": {
title: "Secrets",
api: "core",
viewFn: newResourceView,
listFn: resource.NewSecretList,
colorerFn: defaultColorer,
},
"sts": {
title: "StatefulSets",
api: "apps",
viewFn: newResourceView,
listFn: resource.NewStatefulSetList,
colorerFn: stsColorer,
},
"svc": {
title: "Services",
api: "core",
viewFn: newResourceView,
listFn: resource.NewServiceList,
colorerFn: defaultColorer,
},
}
}

View File

@ -17,7 +17,10 @@ import (
"github.com/rs/zerolog/log"
)
const noSelection = ""
const (
refreshDelay = 0.1
noSelection = ""
)
type (
details interface {
@ -77,7 +80,7 @@ func (v *resourceView) init(ctx context.Context, ns string) {
v.selectedItem, v.selectedNS = noSelection, ns
go func(ctx context.Context) {
initTick := 0.1
initTick := refreshDelay
for {
select {
case <-ctx.Done():
@ -169,8 +172,8 @@ func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := v.getSelectedItem()
raw, err := v.list.Resource().Describe(v.title, sel)
if err != nil {
v.app.flash(flashErr, "Unable to describeCmd this resource", err.Error())
log.Error().Err(err)
v.app.flash(flashErr, err.Error())
log.Warn().Msg(err.Error())
return evt
}
details := v.GetPrimitive("details").(*detailsView)
@ -250,8 +253,6 @@ func (v *resourceView) doSwitchNamespace(ns string) {
config.Root.Save()
}
// Utils...
func (v *resourceView) refresh() {
if _, ok := v.CurrentPage().Item.(*tableView); !ok {
return
@ -263,6 +264,7 @@ func (v *resourceView) refresh() {
v.list.SetNamespace(v.selectedNS)
}
if err := v.list.Reconcile(); err != nil {
log.Warn().Msgf("%s", err)
v.app.flash(flashErr, err.Error())
}
data := v.list.Data()
@ -272,7 +274,7 @@ func (v *resourceView) refresh() {
v.getTV().update(data)
v.selectItem(v.selectedRow, 0)
v.refreshActions()
v.app.infoView.refresh()
v.app.clusterInfoView.refresh()
v.app.Draw()
}
v.update.Unlock()
@ -334,49 +336,51 @@ func (v *resourceView) refreshActions() {
return
}
nn, err := k8s.NewNamespace().List(resource.AllNamespaces)
if err != nil {
v.app.flash(flashErr, "Unable to retrieve namespaces", err.Error())
return
}
if v.list.Namespaced() && !v.list.AllNamespaces() {
if !config.InNSList(nn, v.list.GetNamespace()) {
v.list.SetNamespace(resource.DefaultNamespace)
}
}
var nn []interface{}
aa := make(keyActions)
if v.list.Access(resource.NamespaceAccess) {
v.namespaces = make(map[int]string, config.MaxFavoritesNS)
for i, n := range config.Root.FavNamespaces() {
aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd)
v.namespaces[i] = n
if k8s.CanIAccess("", "list", "namespaces", "namespace.v1") {
var err error
nn, err = k8s.NewNamespace().List(resource.AllNamespaces)
if err != nil {
log.Warn().Msgf("%s", err)
v.app.flash(flashErr, err.Error())
}
if v.list.Namespaced() && !v.list.AllNamespaces() {
if !config.InNSList(nn, v.list.GetNamespace()) {
v.list.SetNamespace(resource.DefaultNamespace)
}
}
if v.list.Access(resource.NamespaceAccess) {
v.namespaces = make(map[int]string, config.MaxFavoritesNS)
for i, n := range config.Root.FavNamespaces() {
aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true)
v.namespaces[i] = n
}
}
}
aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd)
aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false)
aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false)
aa[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
if v.list.Access(resource.EditAccess) {
aa[KeyE] = newKeyAction("Edit", v.editCmd)
aa[KeyE] = newKeyAction("Edit", v.editCmd, true)
}
if v.list.Access(resource.DeleteAccess) {
aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd)
aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true)
}
if v.list.Access(resource.ViewAccess) {
aa[KeyV] = newKeyAction("View", v.viewCmd)
aa[KeyV] = newKeyAction("View", v.viewCmd, true)
}
if v.list.Access(resource.DescribeAccess) {
aa[KeyD] = newKeyAction("Describe", v.describeCmd)
aa[KeyD] = newKeyAction("Describe", v.describeCmd, true)
}
aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd)
if v.extraActionsFn != nil {
v.extraActionsFn(aa)
}
t := v.getTV()
t.setActions(aa)
v.app.setHints(t.hints())

View File

@ -32,13 +32,13 @@ var logo = []string{
}
// Splash screen definition
type Splash struct {
type splashView struct {
*tview.Flex
}
// NewSplash instantiates a new splash screen with product and company info.
func NewSplash(rev string) *Splash {
v := Splash{tview.NewFlex()}
func newSplash(rev string) *splashView {
v := splashView{tview.NewFlex()}
logo := tview.NewTextView()
{
@ -62,11 +62,11 @@ func NewSplash(rev string) *Splash {
return &v
}
func (v *Splash) layoutLogo(t *tview.TextView) {
func (v *splashView) layoutLogo(t *tview.TextView) {
logo := strings.Join(logo, "\n[orange::b]")
fmt.Fprintf(t, "%s[orange::b]%s\n", strings.Repeat("\n", 2), logo)
}
func (v *Splash) layoutRev(t *tview.TextView, rev string) {
func (v *splashView) layoutRev(t *tview.TextView, rev string) {
fmt.Fprintf(t, "[white::b]Revision [red::b]%s", rev)
}

View File

@ -53,14 +53,14 @@ func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView
v.SetInputCapture(v.keyboard)
}
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd)
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.filterCmd)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd)
v.actions[KeyG] = newKeyAction("Top", app.puntCmd)
v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd)
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd)
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.filterCmd, false)
v.actions[tcell.KeyEscape] = newKeyAction("Reset Filter", v.resetCmd, false)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[KeyG] = newKeyAction("Top", app.puntCmd, false)
v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false)
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false)
return &v
}
@ -77,6 +77,7 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
v.cmdBuff.add(evt.Rune())
v.clearSelection()
v.doUpdate(v.filtered())
v.setSelection()
return nil
}
key = tcell.Key(evt.Rune())
@ -89,6 +90,12 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
func (v *tableView) setSelection() {
if v.GetRowCount() > 0 {
v.Select(1, 0)
}
}
func (v *tableView) pageUpCmd(evt *tcell.EventKey) *tcell.EventKey {
v.PageUp()
return nil
@ -113,7 +120,9 @@ func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
v.app.flash(flashInfo, "Filtering off...")
if !v.cmdBuff.empty() {
v.app.flash(flashInfo, "Clearing filter...")
}
v.cmdBuff.reset()
v.refresh()
return nil