rel 0.25.0

mine
derailed 2021-11-15 23:28:03 -07:00
commit e4eaf4d047
106 changed files with 2502 additions and 1080 deletions

View File

@ -278,7 +278,8 @@ K9s uses aliases to navigate most K8s resources.
## K9s Configuration ## K9s Configuration
K9s keeps its configurations inside of a `k9s` directory and the location depends on your operating system. K9s leverages [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) to load its various configurations files. K9s keeps its configurations inside of a `k9s` directory and the location depends on your operating system. K9s leverages [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) to load its various configurations files. For information on the default locations for your OS please see [this link](https://github.com/adrg/xdg/blob/master/README.md). If you are still confused a quick `k9s info` will reveal where k9s is loading its configurations from.
| Unix | macOS | Windows | | Unix | macOS | Windows |
|-----------------|-----------------------------|-----------------------| |-----------------|-----------------------------|-----------------------|

View File

@ -14,7 +14,7 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv
## ♫ Sounds Behind The Release ♭ ## ♫ Sounds Behind The Release ♭
* [High Fidelity - By Elvis Costello (yup! he started as a computer operator. Can u tell?)](https://www.youtube.com/watch?v=DJS-2kacmpU) * [High Fidelity - By Elvis Costello (Yup! he started is career as a computer operator. Can u tell??)](https://www.youtube.com/watch?v=DJS-2kacmpU)
* [Walk With A Big Stick - Foster The People](https://www.youtube.com/watch?v=XMY1VMTyl8s) * [Walk With A Big Stick - Foster The People](https://www.youtube.com/watch?v=XMY1VMTyl8s)
* [Beirut - Steps Ahead -- Love this band!! with the ever so talented and sadly late Michael Brecker ;(](https://www.youtube.com/watch?v=UExKTZ3veB8) * [Beirut - Steps Ahead -- Love this band!! with the ever so talented and sadly late Michael Brecker ;(](https://www.youtube.com/watch?v=UExKTZ3veB8)
@ -25,20 +25,31 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv
I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`! I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!
* [Andrew Regan](https://github.com/poblish) * [Andrew Regan](https://github.com/poblish)
* [Astraea](https://github.com/s22s) * [Bruno Brito](https://github.com/brunohbrito)
* [DataRoots](https://github.com/datarootsio) * [ScubaDrew](https://github.com/ScubaDrew)
* [mike-code](https://github.com/mike-code)
* [Andrew Aadland](https://github.com/DaemonDude23)
* [Michael Albers](https://github.com/michaeljohnalbers)
So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large. So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large.
Also please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!
Thank you!! Thank you!!
--- ---
## Forward That! ## Personal Note...
Ever been in a situation where you need to constantly port-forward on a given pod with multiple containers exposing multiple ports? If so it might be cumbersome to have to type in the full container:port specification to activate a forward. If you fall in this use cases, you can now specify which container and port you would rather port-forward to by default. In this drop, we introduce a new annotation that you can use to specify and container/port to forward to by default. If set the port-forward dialog will know default to your settings. I had so many distractions this cycle so expect some `disturbance in the farce!` on this drop.
To boot rat holed quiet a bit on improving speed. So I might have drop some stuff on the floor in the process...
Please report back if that's the case and we will address shortly. Tx!!
> NOTE: you can either use a port name or number in your annotation. ## Port It Forward??
Ever been in a situation where you need to constantly port-forward on a given pod with multiple containers or exposing multiple ports? If so it might be cumbersome to have to type in the full container:port specification to activate a forward. If you fall in this use cases, you can now specify which container and port you would rather port-forward to by default. In this drop, we introduce a new annotation that you can use to specify and container/port to forward to by default. If set, the port-forward dialog will know to default to your settings.
> NOTE: you can either use a container port name or number in your annotation!
```yaml ```yaml
# Pod fred # Pod fred
@ -47,9 +58,10 @@ kind: Pod
metadata: metadata:
name: fred name: fred
annotations: annotations:
k9s.imhotep.io/default-portforward-container: bozo:p1 # => will default to container bozo port named p1 k9scli.io/auto-portforwards: zorg::5556 # => will default to container zorg port 5556 and local port 5566. No port-forward dialog will be shown.
# Or... # Or...
k9s.imhotep.io/default-portforward-container: bozo:8081 # => will default to container bozo port number 8081 k9scli.io/portforward: bozo::6666:p1 # => launches the port-forward dialog selecting default port-forward on container bozo port named p1(8081)
# mapping to local port 6666.
... ...
spec: spec:
containers: containers:
@ -67,18 +79,35 @@ spec:
... ...
``` ```
The annotation value must specify a container to forward to as well as a local port and container port. The container port may be specified as either a port number or port name. If the local port is omitted then the local port will default to the container port number. Here are a few examples:
1. bozo::http - creates a pf on container `bozo` with port name http. If http specifies port number 8080 then the local port will be 8080 as well.
2. bozo::9090:http - creates a pf on container `bozo` mapping local port 9090->http(8080)
3. bozo::9090:8080 - creates a pf on container `bozo` mapping local port 9090->8080
--- ---
## Resolved Issues ## Resolved Issues
* [Issue #1299](https://github.com/derailed/k9s/issues/1299) After upgrade to 0.24.15 sorting shortcuts not working
* [Issue #1298](https://github.com/derailed/k9s/issues/1298) Install K9s through go get reporting ambiguous import error
* [Issue #1296](https://github.com/derailed/k9s/issues/1296) Crash when clicking between border of K9s and terminal pane
* [Issue #1289](https://github.com/derailed/k9s/issues/1289) Homebrew calling bottle :unneeded is deprecated! There is no replacement
* [Issue #1273](https://github.com/derailed/k9s/issues/1273) Not loading config from correct default location when XDG_CONFIG_HOME is unset
* [Issue #1268](https://github.com/derailed/k9s/issues/1268) Age sorting wrong for years * [Issue #1268](https://github.com/derailed/k9s/issues/1268) Age sorting wrong for years
* [Issue #1258](https://github.com/derailed/k9s/issues/1258) Configurable or recent use based port-forward * [Issue #1258](https://github.com/derailed/k9s/issues/1258) Configurable or recent use based port-forward
* [Issue #1257](https://github.com/derailed/k9s/issues/1257) Why is the latest chocolatey on 0.24.10 * [Issue #1257](https://github.com/derailed/k9s/issues/1257) Why is the latest chocolatey on 0.24.10
* [Issue #1243](https://github.com/derailed/k9s/issues/1243) Port forward fails in kind on windows 10
--- ---
## PRs ## PRs
* [PR #1300](https://github.com/derailed/k9s/pull/1300) move from io/ioutil to io/os packages
* [PR #1287](https://github.com/derailed/k9s/pull/1287) Add missing styles to kiss
* [PR #1286](https://github.com/derailed/k9s/pull/1286) Some small color modifications
* [PR #1284](https://github.com/derailed/k9s/pull/1284) Fix a small typo which comes from cluster view info
* [PR #1271](https://github.com/derailed/k9s/pull/1271) Removed cursor colors that are too light to read
* [PR #1266](https://github.com/derailed/k9s/pull/1266) Skin to preserve your terminal session background color * [PR #1266](https://github.com/derailed/k9s/pull/1266) Skin to preserve your terminal session background color
* [PR #1264](https://github.com/derailed/k9s/pull/1205) Adding note on popeye config * [PR #1264](https://github.com/derailed/k9s/pull/1205) Adding note on popeye config
* [PR #1261](https://github.com/derailed/k9s/pull/1261) Blurry logo * [PR #1261](https://github.com/derailed/k9s/pull/1261) Blurry logo

View File

@ -94,25 +94,25 @@ func loadConfiguration() *config.Config {
k9sCfg.K9s.OverrideWrite(*k9sFlags.Write) k9sCfg.K9s.OverrideWrite(*k9sFlags.Write)
k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) k9sCfg.K9s.OverrideCommand(*k9sFlags.Command)
if err := k9sCfg.Refine(k8sFlags, k9sFlags); err != nil { if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
log.Error().Err(err).Msgf("refine failed") log.Error().Err(err).Msgf("refine failed")
} }
conn, err := client.InitConnection(k8sCfg) conn, err := client.InitConnection(k8sCfg)
k9sCfg.SetConnection(conn) k9sCfg.SetConnection(conn)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("failed to connect to cluster") log.Error().Err(err).Msgf("failed to connect to cluster")
} else { return k9sCfg
// Try to access server version if that fail. Connectivity issue? }
if !k9sCfg.GetConnection().CheckConnectivity() { // Try to access server version if that fail. Connectivity issue?
log.Panic().Msgf("K9s can't connect to cluster") if !k9sCfg.GetConnection().CheckConnectivity() {
} log.Panic().Msgf("Cannot connect to cluster")
if !k9sCfg.GetConnection().ConnectionOK() { }
panic("No connectivity") if !k9sCfg.GetConnection().ConnectionOK() {
} panic("No connectivity")
log.Info().Msg("✅ Kubernetes connectivity") }
if err := k9sCfg.Save(); err != nil { log.Info().Msg("✅ Kubernetes connectivity")
log.Error().Err(err).Msg("Config save") if err := k9sCfg.Save(); err != nil {
} log.Error().Err(err).Msg("Config save")
} }
return k9sCfg return k9sCfg

6
go.mod
View File

@ -14,7 +14,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v4 v4.1.1 github.com/cenkalti/backoff/v4 v4.1.1
github.com/derailed/popeye v0.9.7 github.com/derailed/popeye v0.9.7
github.com/derailed/tview v0.6.1 github.com/derailed/tview v0.6.3
github.com/fatih/color v1.12.0 github.com/fatih/color v1.12.0
github.com/fsnotify/fsnotify v1.5.1 github.com/fsnotify/fsnotify v1.5.1
github.com/fvbommel/sortorder v1.0.2 github.com/fvbommel/sortorder v1.0.2
@ -22,10 +22,6 @@ require (
github.com/ghodss/yaml v1.0.0 github.com/ghodss/yaml v1.0.0
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.13 github.com/mattn/go-runewidth v0.0.13
// BOZO!! revamp with latest...
// github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
// github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
// github.com/openfaas/faas-provider v0.15.0
github.com/petergtz/pegomock v2.9.0+incompatible github.com/petergtz/pegomock v2.9.0+incompatible
github.com/rakyll/hey v0.1.4 github.com/rakyll/hey v0.1.4
github.com/rs/zerolog v1.25.0 github.com/rs/zerolog v1.25.0

5
go.sum
View File

@ -216,8 +216,8 @@ github.com/derailed/popeye v0.9.7 h1:EnOl8rwvAlN4KJo62+V7J713ZAWFQmKCrTBBBBBkbmQ
github.com/derailed/popeye v0.9.7/go.mod h1:Ih3wTG7wBOuxdqz5tlCuCFq/vyB+Te/IpqY5HwgUTEA= github.com/derailed/popeye v0.9.7/go.mod h1:Ih3wTG7wBOuxdqz5tlCuCFq/vyB+Te/IpqY5HwgUTEA=
github.com/derailed/tcell/v2 v2.3.1-rc.2 h1:9TmZB/IwL3MA1Jf4pC4rfMaPTcVYIN62IwE7X7A9emU= github.com/derailed/tcell/v2 v2.3.1-rc.2 h1:9TmZB/IwL3MA1Jf4pC4rfMaPTcVYIN62IwE7X7A9emU=
github.com/derailed/tcell/v2 v2.3.1-rc.2/go.mod h1:wegJ+SscH+jPjEQIAV/dI/grLTRm5R4IE2M479NDSL0= github.com/derailed/tcell/v2 v2.3.1-rc.2/go.mod h1:wegJ+SscH+jPjEQIAV/dI/grLTRm5R4IE2M479NDSL0=
github.com/derailed/tview v0.6.1 h1:dB+9bO7r6a1Yg1HE+XNJj61hioauJnGBFq2biC5bjAk= github.com/derailed/tview v0.6.3 h1:4GFzcmuVjHYHKlLEpU8lSiUBVfHeYQEC0z5tlBLp4CI=
github.com/derailed/tview v0.6.1/go.mod h1:5Wjopun0Jw3zxOFtafwc/GlrkFJix1hZz1oQetWpnwE= github.com/derailed/tview v0.6.3/go.mod h1:j2GwRsCb3NZe7lRjKIeplvZLkg8duyNWG6I4y+bZwEE=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
@ -1144,7 +1144,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -63,7 +63,7 @@ func InitConnection(config *Config) (*APIClient, error) {
} }
err := a.supportsMetricsResources() err := a.supportsMetricsResources()
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Checking metrics-server") log.Error().Err(err).Msgf("Fail to locate metrics-server")
} }
if errors.Is(err, noMetricServerErr) || errors.Is(err, metricsUnsupportedErr) { if errors.Is(err, noMetricServerErr) || errors.Is(err, metricsUnsupportedErr) {
return &a, nil return &a, nil
@ -120,11 +120,11 @@ func (a *APIClient) IsActiveNamespace(ns string) bool {
// ActiveNamespace returns the current namespace. // ActiveNamespace returns the current namespace.
func (a *APIClient) ActiveNamespace() string { func (a *APIClient) ActiveNamespace() string {
ns, err := a.CurrentNamespaceName() if ns, err := a.CurrentNamespaceName(); err == nil {
if err != nil { return ns
return AllNamespaces
} }
return ns
return AllNamespaces
} }
func (a *APIClient) clearCache() { func (a *APIClient) clearCache() {
@ -261,7 +261,7 @@ func (a *APIClient) CheckConnectivity() bool {
a.reset() a.reset()
} }
} else { } else {
log.Error().Err(err).Msgf("K9s can't connect to cluster") log.Error().Err(err).Msgf("can't connect to cluster")
a.connOK = false a.connOK = false
} }
@ -301,11 +301,7 @@ func (a *APIClient) Dial() (kubernetes.Interface, error) {
// RestConfig returns a rest api client. // RestConfig returns a rest api client.
func (a *APIClient) RestConfig() (*restclient.Config, error) { func (a *APIClient) RestConfig() (*restclient.Config, error) {
cfg, err := a.config.RESTConfig() return a.config.RESTConfig()
if err != nil {
return nil, err
}
return cfg, nil
} }
// CachedDiscovery returns a cached discovery client. // CachedDiscovery returns a cached discovery client.
@ -430,7 +426,6 @@ func (a *APIClient) supportsMetricsResources() error {
} }
apiGroups, err := dial.ServerGroups() apiGroups, err := dial.ServerGroups()
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("Unable to fetch APIGroups")
return err return err
} }
for _, grp := range apiGroups.Groups { for _, grp := range apiGroups.Groups {

View File

@ -15,32 +15,30 @@ import (
) )
const ( const (
defaultQPS = 50
defaultBurst = 50
defaultCallTimeoutDuration time.Duration = 5 * time.Second defaultCallTimeoutDuration time.Duration = 5 * time.Second
) )
// Config tracks a kubernetes configuration. // Config tracks a kubernetes configuration.
type Config struct { type Config struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
clientConfig clientcmd.ClientConfig clientCfg clientcmd.ClientConfig
rawConfig *clientcmdapi.Config rawCfg *clientcmdapi.Config
restConfig *restclient.Config mutex *sync.RWMutex
mutex *sync.RWMutex OverrideNS bool
OverrideNS bool
} }
// NewConfig returns a new k8s config or an error if the flags are invalid. // NewConfig returns a new k8s config or an error if the flags are invalid.
func NewConfig(f *genericclioptions.ConfigFlags) *Config { func NewConfig(f *genericclioptions.ConfigFlags) *Config {
return &Config{ return &Config{
flags: f, flags: f,
// pathOptions: clientcmd.NewDefaultPathOptions(),
mutex: &sync.RWMutex{}, mutex: &sync.RWMutex{},
} }
} }
// CallTimeout returns the call timeout if set or the default if not set. // CallTimeout returns the call timeout if set or the default if not set.
func (c *Config) CallTimeout() time.Duration { func (c *Config) CallTimeout() time.Duration {
if c.flags.Timeout == nil { if !isSet(c.flags.Timeout) {
return defaultCallTimeoutDuration return defaultCallTimeoutDuration
} }
dur, err := time.ParseDuration(*c.flags.Timeout) dur, err := time.ParseDuration(*c.flags.Timeout)
@ -51,28 +49,63 @@ func (c *Config) CallTimeout() time.Duration {
return dur return dur
} }
func (c *Config) RESTConfig() (*restclient.Config, error) {
return c.clientConfig().ClientConfig()
}
// Flags returns configuration flags. // Flags returns configuration flags.
func (c *Config) Flags() *genericclioptions.ConfigFlags { func (c *Config) Flags() *genericclioptions.ConfigFlags {
return c.flags return c.flags
} }
// SwitchContext changes the kubeconfig context to a new cluster. func (c *Config) rawConfig() (*clientcmdapi.Config, error) {
func (c *Config) SwitchContext(name string) error { if c.rawCfg != nil {
if c.flags.Context != nil && *c.flags.Context == name { return c.rawCfg, nil
return nil
} }
if _, err := c.GetContext(name); err != nil { cfg, err := c.clientConfig().RawConfig()
if err != nil {
return nil, err
}
c.rawCfg = &cfg
return c.rawCfg, nil
}
func (c *Config) clientConfig() clientcmd.ClientConfig {
if c.clientCfg != nil {
return c.clientCfg
}
c.clientCfg = c.flags.ToRawKubeConfigLoader()
return c.clientCfg
}
func (c *Config) reset() {
c.clientCfg = nil
}
// SwitchContext changes the kubeconfig context to a new cluster.
func (c *Config) SwitchContext(name string) error {
cfg, err := c.rawConfig()
if err != nil {
return err
}
if cfg.CurrentContext == name {
return nil
}
context, err := c.GetContext(name)
if err != nil {
return fmt.Errorf("context %s does not exist", name) return fmt.Errorf("context %s does not exist", name)
} }
c.reset()
c.flags.Context = &name c.flags.Context = &name
c.flags.ClusterName = &(context.Cluster)
return nil return nil
} }
func (c *Config) reset() { func (c *Config) RawConfig() *clientcmdapi.Config {
c.clientConfig, c.rawConfig, c.restConfig = nil, nil, nil return c.rawCfg
} }
// CurrentContextName returns the currently active config context. // CurrentContextName returns the currently active config context.
@ -80,17 +113,17 @@ func (c *Config) CurrentContextName() (string, error) {
if isSet(c.flags.Context) { if isSet(c.flags.Context) {
return *c.flags.Context, nil return *c.flags.Context, nil
} }
cfg, err := c.rawConfig()
cfg, err := c.RawConfig()
if err != nil { if err != nil {
return "", err return "", err
} }
return cfg.CurrentContext, nil return cfg.CurrentContext, nil
} }
// GetContext fetch a given context or error if it does not exists. // GetContext fetch a given context or error if it does not exists.
func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) { func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) {
cfg, err := c.RawConfig() cfg, err := c.rawConfig()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -103,7 +136,7 @@ func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) {
// Contexts fetch all available contexts. // Contexts fetch all available contexts.
func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) { func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) {
cfg, err := c.RawConfig() cfg, err := c.rawConfig()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -113,18 +146,23 @@ func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) {
// DelContext remove a given context from the configuration. // DelContext remove a given context from the configuration.
func (c *Config) DelContext(n string) error { func (c *Config) DelContext(n string) error {
cfg, err := c.RawConfig() cfg, err := c.rawConfig()
if err != nil { if err != nil {
return err return err
} }
delete(cfg.Contexts, n) delete(cfg.Contexts, n)
return clientcmd.ModifyConfig(c.clientConfig.ConfigAccess(), cfg, true) acc, err := c.ConfigAccess()
if err != nil {
return err
}
return clientcmd.ModifyConfig(acc, *cfg, true)
} }
// ContextNames fetch all available contexts. // ContextNames fetch all available contexts.
func (c *Config) ContextNames() ([]string, error) { func (c *Config) ContextNames() ([]string, error) {
cfg, err := c.RawConfig() cfg, err := c.rawConfig()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -137,16 +175,16 @@ func (c *Config) ContextNames() ([]string, error) {
} }
// ClusterNameFromContext returns the cluster associated with the given context. // ClusterNameFromContext returns the cluster associated with the given context.
func (c *Config) ClusterNameFromContext(ctx string) (string, error) { func (c *Config) ClusterNameFromContext(context string) (string, error) {
cfg, err := c.RawConfig() cfg, err := c.rawConfig()
if err != nil { if err != nil {
return "", err return "", err
} }
if ctx, ok := cfg.Contexts[ctx]; ok { if ctx, ok := cfg.Contexts[context]; ok {
return ctx.Cluster, nil return ctx.Cluster, nil
} }
return "", fmt.Errorf("unable to locate cluster from context %s", ctx) return "", fmt.Errorf("unable to locate cluster from context %s", context)
} }
// CurrentClusterName returns the active cluster name. // CurrentClusterName returns the active cluster name.
@ -154,16 +192,11 @@ func (c *Config) CurrentClusterName() (string, error) {
if isSet(c.flags.ClusterName) { if isSet(c.flags.ClusterName) {
return *c.flags.ClusterName, nil return *c.flags.ClusterName, nil
} }
cfg, err := c.rawConfig()
cfg, err := c.RawConfig()
if err != nil { if err != nil {
return "", err return "", err
} }
current := cfg.CurrentContext current := cfg.CurrentContext
if isSet(c.flags.Context) {
current = *c.flags.Context
}
if ctx, ok := cfg.Contexts[current]; ok { if ctx, ok := cfg.Contexts[current]; ok {
return ctx.Cluster, nil return ctx.Cluster, nil
@ -174,7 +207,7 @@ func (c *Config) CurrentClusterName() (string, error) {
// ClusterNames fetch all kubeconfig defined clusters. // ClusterNames fetch all kubeconfig defined clusters.
func (c *Config) ClusterNames() ([]string, error) { func (c *Config) ClusterNames() ([]string, error) {
cfg, err := c.RawConfig() cfg, err := c.rawConfig()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -224,7 +257,7 @@ func (c *Config) CurrentUserName() (string, error) {
return *c.flags.AuthInfoName, nil return *c.flags.AuthInfoName, nil
} }
cfg, err := c.RawConfig() cfg, err := c.rawConfig()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -242,25 +275,9 @@ func (c *Config) CurrentUserName() (string, error) {
// CurrentNamespaceName retrieves the active namespace. // CurrentNamespaceName retrieves the active namespace.
func (c *Config) CurrentNamespaceName() (string, error) { func (c *Config) CurrentNamespaceName() (string, error) {
if isSet(c.flags.Namespace) { ns, _, err := c.clientConfig().Namespace()
return *c.flags.Namespace, nil
}
cfg, err := c.RawConfig() return ns, err
if err != nil {
return "", err
}
ctx, err := c.CurrentContextName()
if err != nil {
return "", err
}
if ctx, ok := cfg.Contexts[ctx]; ok {
if isSet(&ctx.Namespace) {
return ctx.Namespace, nil
}
}
return "", fmt.Errorf("No active namespace specified")
} }
// NamespaceNames fetch all available namespaces on current cluster. // NamespaceNames fetch all available namespaces on current cluster.
@ -278,51 +295,7 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) {
c.mutex.RLock() c.mutex.RLock()
defer c.mutex.RUnlock() defer c.mutex.RUnlock()
c.ensureConfig() return c.clientConfig().ConfigAccess(), nil
return c.clientConfig.ConfigAccess(), nil
}
// RawConfig fetch the current kubeconfig with no overrides.
func (c *Config) RawConfig() (clientcmdapi.Config, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.rawConfig == nil {
c.ensureConfig()
cfg, err := c.clientConfig.RawConfig()
if err != nil {
return cfg, err
}
c.rawConfig = &cfg
if c.flags.Context == nil {
c.flags.Context = &c.rawConfig.CurrentContext
}
}
return *c.rawConfig, nil
}
// RESTConfig fetch the current REST api service connection.
func (c *Config) RESTConfig() (*restclient.Config, error) {
if c.restConfig != nil {
return c.restConfig, nil
}
var err error
if c.restConfig, err = c.flags.ToRESTConfig(); err != nil {
return nil, err
}
c.restConfig.QPS = defaultQPS
c.restConfig.Burst = defaultBurst
return c.restConfig, nil
}
func (c *Config) ensureConfig() {
if c.clientConfig != nil {
return
}
c.clientConfig = c.flags.ToRawKubeConfigLoader()
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -2,7 +2,6 @@ package client_test
import ( import (
"errors" "errors"
"fmt"
"testing" "testing"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -18,98 +17,151 @@ func init() {
} }
func TestConfigCurrentContext(t *testing.T) { func TestConfigCurrentContext(t *testing.T) {
name, kubeConfig := "blee", "./testdata/config" var kubeConfig = "./testdata/config"
uu := []struct {
flags *genericclioptions.ConfigFlags uu := map[string]struct {
context string context string
e string
}{ }{
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"}, "default": {
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &name}, "blee"}, e: "fred",
},
"custom": {
context: "blee",
e: "blee",
},
} }
for _, u := range uu { for k := range uu {
cfg := client.NewConfig(u.flags) u := uu[k]
ctx, err := cfg.CurrentContextName() t.Run(k, func(t *testing.T) {
assert.Nil(t, err) flags := genericclioptions.NewConfigFlags(false)
assert.Equal(t, u.context, ctx) flags.KubeConfig = &kubeConfig
if u.context != "" {
flags.Context = &u.context
}
cfg := client.NewConfig(flags)
ctx, err := cfg.CurrentContextName()
assert.Nil(t, err)
assert.Equal(t, u.e, ctx)
})
} }
} }
func TestConfigCurrentCluster(t *testing.T) { func TestConfigCurrentCluster(t *testing.T) {
name, kubeConfig := "blee", "./testdata/config" name, kubeConfig := "blee", "./testdata/config"
uu := []struct { uu := map[string]struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
cluster string cluster string
}{ }{
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"}, "default": {
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name}, "blee"}, flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},
cluster: "fred",
},
"custom": {
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name},
cluster: "blee",
},
} }
for _, u := range uu { for k := range uu {
cfg := client.NewConfig(u.flags) u := uu[k]
ctx, err := cfg.CurrentClusterName() t.Run(k, func(t *testing.T) {
assert.Nil(t, err) cfg := client.NewConfig(u.flags)
assert.Equal(t, u.cluster, ctx) ctx, err := cfg.CurrentClusterName()
assert.Nil(t, err)
assert.Equal(t, u.cluster, ctx)
})
} }
} }
func TestConfigCurrentUser(t *testing.T) { func TestConfigCurrentUser(t *testing.T) {
name, kubeConfig := "blee", "./testdata/config" name, kubeConfig := "blee", "./testdata/config"
uu := []struct { uu := map[string]struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
user string user string
}{ }{
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"}, "default": {
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, AuthInfoName: &name}, "blee"}, flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},
user: "fred",
},
"custom": {
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, AuthInfoName: &name},
user: "blee",
},
} }
for _, u := range uu { for k := range uu {
cfg := client.NewConfig(u.flags) u := uu[k]
ctx, err := cfg.CurrentUserName() t.Run(k, func(t *testing.T) {
assert.Nil(t, err) cfg := client.NewConfig(u.flags)
assert.Equal(t, u.user, ctx) ctx, err := cfg.CurrentUserName()
assert.Nil(t, err)
assert.Equal(t, u.user, ctx)
})
} }
} }
func TestConfigCurrentNamespace(t *testing.T) { func TestConfigCurrentNamespace(t *testing.T) {
name, kubeConfig := "blee", "./testdata/config" kubeConfig := "./testdata/config"
uu := []struct { bleeNS, bleeCTX := "blee", "blee"
uu := map[string]struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
namespace string namespace string
err error
}{ }{
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "", fmt.Errorf("No active namespace specified")}, "default": {
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &name}, "blee", nil}, flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},
namespace: "default",
},
"withContext": {
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &bleeCTX},
namespace: "zorg",
},
"withNS": {
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &bleeNS},
namespace: "blee",
},
} }
for _, u := range uu { for k := range uu {
cfg := client.NewConfig(u.flags) u := uu[k]
ns, err := cfg.CurrentNamespaceName() t.Run(k, func(t *testing.T) {
assert.Equal(t, u.err, err) cfg := client.NewConfig(u.flags)
assert.Equal(t, u.namespace, ns) ns, err := cfg.CurrentNamespaceName()
assert.Nil(t, err)
assert.Equal(t, u.namespace, ns)
})
} }
} }
func TestConfigGetContext(t *testing.T) { func TestConfigGetContext(t *testing.T) {
kubeConfig := "./testdata/config" kubeConfig := "./testdata/config"
uu := []struct { uu := map[string]struct {
flags *genericclioptions.ConfigFlags
cluster string cluster string
err error err error
}{ }{
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "blee", nil}, "default": {
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "bozo", errors.New("invalid context `bozo specified")}, cluster: "blee",
},
"custom": {
cluster: "bozo",
err: errors.New("invalid context `bozo specified"),
},
} }
for _, u := range uu { flags := &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}
cfg := client.NewConfig(u.flags) cfg := client.NewConfig(flags)
ctx, err := cfg.GetContext(u.cluster) for k := range uu {
if err != nil { u := uu[k]
assert.Equal(t, u.err, err) t.Run(k, func(t *testing.T) {
} else { ctx, err := cfg.GetContext(u.cluster)
assert.NotNil(t, ctx) if err != nil {
assert.Equal(t, u.cluster, ctx.Cluster) assert.Equal(t, u.err, err)
} } else {
assert.NotNil(t, ctx)
assert.Equal(t, u.cluster, ctx.Cluster)
}
})
} }
} }
@ -205,7 +257,8 @@ func TestConfigDelContext(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
cc, err := cfg.ContextNames() cc, err := cfg.ContextNames()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, len(cc)) assert.Equal(t, 1, len(cc))
assert.Equal(t, "blee", cc[0])
} }
func TestConfigRestConfig(t *testing.T) { func TestConfigRestConfig(t *testing.T) {

View File

@ -2,42 +2,43 @@ apiVersion: v1
kind: Config kind: Config
preferences: {} preferences: {}
clusters: clusters:
- cluster: - cluster:
insecure-skip-tls-verify: true insecure-skip-tls-verify: true
server: https://localhost:3000 server: https://localhost:3000
name: fred name: fred
- cluster: - cluster:
insecure-skip-tls-verify: true insecure-skip-tls-verify: true
server: https://localhost:3001 server: https://localhost:3001
name: blee name: blee
- cluster: - cluster:
insecure-skip-tls-verify: true insecure-skip-tls-verify: true
server: https://localhost:3002 server: https://localhost:3002
name: duh name: duh
contexts: contexts:
- context: - context:
cluster: fred cluster: fred
user: fred user: fred
name: fred name: fred
- context: - context:
cluster: blee cluster: blee
user: blee user: blee
name: blee namespace: zorg
- context: name: blee
cluster: duh - context:
user: duh cluster: duh
name: duh user: duh
name: duh
current-context: fred current-context: fred
users: users:
- name: fred - name: fred
user: user:
client-certificate-data: ZnJlZA== client-certificate-data: ZnJlZA==
client-key-data: ZnJlZA== client-key-data: ZnJlZA==
- name: blee - name: blee
user: user:
client-certificate-data: ZnJlZA== client-certificate-data: ZnJlZA==
client-key-data: ZnJlZA== client-key-data: ZnJlZA==
- name: duh - name: duh
user: user:
client-certificate-data: ZnJlZA== client-certificate-data: ZnJlZA==
client-key-data: ZnJlZA== client-key-data: ZnJlZA==

View File

@ -7,33 +7,13 @@ clusters:
- cluster: - cluster:
insecure-skip-tls-verify: true insecure-skip-tls-verify: true
server: https://localhost:3002 server: https://localhost:3002
name: duh
- cluster:
insecure-skip-tls-verify: true
server: https://localhost:3000
name: fred name: fred
contexts: contexts:
- context: - context:
cluster: blee cluster: blee
user: blee user: blee
name: blee name: blee
- context: current-context: blee
cluster: duh
user: duh
name: duh
current-context: fred
kind: Config kind: Config
preferences: {} preferences: {}
users: users: null
- name: blee
user:
client-certificate-data: ZnJlZA==
client-key-data: ZnJlZA==
- name: duh
user:
client-certificate-data: ZnJlZA==
client-key-data: ZnJlZA==
- name: fred
user:
client-certificate-data: ZnJlZA==
client-key-data: ZnJlZA==

View File

@ -1,11 +0,0 @@
package client
// PortTunnel represents a host tunnel port mapper.
type PortTunnel struct {
Address, LocalPort, ContainerPort string
}
// PortMap returns a port mapping.
func (t PortTunnel) PortMap() string {
return t.LocalPort + ":" + t.ContainerPort
}

View File

@ -1,7 +1,7 @@
package config package config
import ( import (
"io/ioutil" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -105,7 +105,7 @@ func (a *Aliases) Load() error {
// LoadFileAliases loads alias from a given file. // LoadFileAliases loads alias from a given file.
func (a *Aliases) LoadFileAliases(path string) error { func (a *Aliases) LoadFileAliases(path string) error {
f, err := ioutil.ReadFile(path) f, err := os.ReadFile(path)
if err == nil { if err == nil {
var aa Aliases var aa Aliases
if err := yaml.Unmarshal(f, &aa); err != nil { if err := yaml.Unmarshal(f, &aa); err != nil {
@ -171,5 +171,5 @@ func (a *Aliases) SaveAliases(path string) error {
if err != nil { if err != nil {
return err return err
} }
return ioutil.WriteFile(path, cfg, 0644) return os.WriteFile(path, cfg, 0644)
} }

View File

@ -1,8 +1,8 @@
package config package config
import ( import (
"io/ioutil"
"net/http" "net/http"
"os"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -96,7 +96,7 @@ func (s *Bench) Reload(path string) error {
// Load K9s benchmark configs from file. // Load K9s benchmark configs from file.
func (s *Bench) load(path string) error { func (s *Bench) load(path string) error {
f, err := ioutil.ReadFile(path) f, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,8 +3,8 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path"
"path/filepath" "path/filepath"
"github.com/adrg/xdg" "github.com/adrg/xdg"
@ -60,6 +60,15 @@ func K9sHome() string {
if env := os.Getenv(K9sConfig); env != "" { if env := os.Getenv(K9sConfig); env != "" {
return env return env
} }
if env := os.Getenv("XDG_CONFIG_HOME"); env == "" {
dir, err := os.UserHomeDir()
if err != nil {
log.Error().Err(err).Msgf("user home dir")
return ""
}
return path.Join(dir, ".config", "k9s")
}
xdgK9sHome, err := xdg.ConfigFile("k9s") xdgK9sHome, err := xdg.ConfigFile("k9s")
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s") log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s")
@ -74,21 +83,25 @@ func NewConfig(ks KubeSettings) *Config {
} }
// Refine the configuration based on cli args. // Refine the configuration based on cli args.
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags) error { func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error {
cfg, err := flags.ToRawKubeConfigLoader().RawConfig()
if err != nil {
return err
}
if isSet(flags.Context) { if isSet(flags.Context) {
c.K9s.CurrentContext = *flags.Context c.K9s.CurrentContext = *flags.Context
} else { } else {
c.K9s.CurrentContext = cfg.CurrentContext context, err := cfg.CurrentContextName()
if err != nil {
return err
}
c.K9s.CurrentContext = context
} }
log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext) log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext)
if c.K9s.CurrentContext == "" { if c.K9s.CurrentContext == "" {
return errors.New("Invalid kubeconfig context detected") return errors.New("Invalid kubeconfig context detected")
} }
context, ok := cfg.Contexts[c.K9s.CurrentContext] cc, err := cfg.Contexts()
if err != nil {
return err
}
context, ok := cc[c.K9s.CurrentContext]
if !ok { if !ok {
return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext) return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext)
} }
@ -218,7 +231,7 @@ func (c *Config) SetConnection(conn client.Connection) {
// Load K9s configuration from file. // Load K9s configuration from file.
func (c *Config) Load(path string) error { func (c *Config) Load(path string) error {
f, err := ioutil.ReadFile(path) f, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }
@ -252,7 +265,7 @@ func (c *Config) SaveFile(path string) error {
log.Error().Msgf("[Config] Unable to save K9s config file: %v", err) log.Error().Msgf("[Config] Unable to save K9s config file: %v", err)
return err return err
} }
return ioutil.WriteFile(path, cfg, 0644) return os.WriteFile(path, cfg, 0644)
} }
// Validate the configuration. // Validate the configuration.

View File

@ -2,10 +2,11 @@ package config_test
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
m "github.com/petergtz/pegomock" m "github.com/petergtz/pegomock"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -63,7 +64,7 @@ func TestConfigRefine(t *testing.T) {
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
cfg := config.NewConfig(mk) cfg := config.NewConfig(mk)
err := cfg.Refine(u.flags, nil) err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
if u.issue { if u.issue {
assert.NotNil(t, err) assert.NotNil(t, err)
} else { } else {
@ -87,7 +88,6 @@ func TestConfigValidate(t *testing.T) {
cfg.SetConnection(mc) cfg.SetConnection(mc)
assert.Nil(t, cfg.Load("testdata/k9s.yml")) assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg.Validate() cfg.Validate()
// mc.VerifyWasCalledOnce().ValidNamespaces()
} }
func TestConfigLoad(t *testing.T) { func TestConfigLoad(t *testing.T) {
@ -216,7 +216,7 @@ func TestConfigSaveFile(t *testing.T) {
err := cfg.SaveFile(path) err := cfg.SaveFile(path)
assert.Nil(t, err) assert.Nil(t, err)
raw, err := ioutil.ReadFile(path) raw, err := os.ReadFile(path)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, expectedConfig, string(raw)) assert.Equal(t, expectedConfig, string(raw))
} }
@ -242,7 +242,7 @@ func TestConfigReset(t *testing.T) {
err := cfg.SaveFile(path) err := cfg.SaveFile(path)
assert.Nil(t, err) assert.Nil(t, err)
raw, err := ioutil.ReadFile(path) raw, err := os.ReadFile(path)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, resetConfig, string(raw)) assert.Equal(t, resetConfig, string(raw))
} }

View File

@ -1,7 +1,7 @@
package config package config
import ( import (
"io/ioutil" "os"
"path/filepath" "path/filepath"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -36,7 +36,7 @@ func (h HotKeys) Load() error {
// LoadHotKeys loads plugins from a given file. // LoadHotKeys loads plugins from a given file.
func (h HotKeys) LoadHotKeys(path string) error { func (h HotKeys) LoadHotKeys(path string) error {
f, err := ioutil.ReadFile(path) f, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,8 +1,10 @@
package config package config
import ( import (
"io/ioutil" "fmt"
"os"
"path/filepath" "path/filepath"
"strings"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -20,12 +22,17 @@ type Plugin struct {
Scopes []string `yaml:"scopes"` Scopes []string `yaml:"scopes"`
Args []string `yaml:"args"` Args []string `yaml:"args"`
ShortCut string `yaml:"shortCut"` ShortCut string `yaml:"shortCut"`
Pipes []string `yaml:"pipes"`
Description string `yaml:"description"` Description string `yaml:"description"`
Command string `yaml:"command"` Command string `yaml:"command"`
Confirm bool `yaml:"confirm"` Confirm bool `yaml:"confirm"`
Background bool `yaml:"background"` Background bool `yaml:"background"`
} }
func (p Plugin) String() string {
return fmt.Sprintf("[%s] %s(%s)", p.ShortCut, p.Command, strings.Join(p.Args, " "))
}
// NewPlugins returns a new plugin. // NewPlugins returns a new plugin.
func NewPlugins() Plugins { func NewPlugins() Plugins {
return Plugins{ return Plugins{
@ -40,7 +47,7 @@ func (p Plugins) Load() error {
// LoadPlugins loads plugins from a given file. // LoadPlugins loads plugins from a given file.
func (p Plugins) LoadPlugins(path string) error { func (p Plugins) LoadPlugins(path string) error {
f, err := ioutil.ReadFile(path) f, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }

View File

@ -2,7 +2,7 @@ package config
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"github.com/derailed/tview" "github.com/derailed/tview"
@ -541,7 +541,7 @@ func (s *Styles) Views() Views {
// Load K9s configuration from file. // Load K9s configuration from file.
func (s *Styles) Load(path string) error { func (s *Styles) Load(path string) error {
f, err := ioutil.ReadFile(path) f, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,7 +1,7 @@
package config package config
import ( import (
"io/ioutil" "os"
"path/filepath" "path/filepath"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -56,7 +56,7 @@ func (v *CustomView) Reset() {
// Load loads view configurations. // Load loads view configurations.
func (v *CustomView) Load(path string) error { func (v *CustomView) Load(path string) error {
raw, err := ioutil.ReadFile(path) raw, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,7 +3,6 @@ package dao
import ( import (
"context" "context"
"errors" "errors"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -41,7 +40,7 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error
} }
path, _ := ctx.Value(internal.KeyPath).(string) path, _ := ctx.Value(internal.KeyPath).(string)
ff, err := ioutil.ReadDir(dir) ff, err := os.ReadDir(dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -51,7 +50,10 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error
if path != "" && !strings.HasPrefix(f.Name(), strings.Replace(path, "/", "_", 1)) { if path != "" && !strings.HasPrefix(f.Name(), strings.Replace(path, "/", "_", 1)) {
continue continue
} }
oo = append(oo, render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())})
if fi, err := f.Info(); err == nil {
oo = append(oo, render.BenchInfo{File: fi, Path: filepath.Join(dir, f.Name())})
}
} }
return oo, nil return oo, nil

View File

@ -2,6 +2,7 @@ package dao
import ( import (
"context" "context"
"errors"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
@ -63,14 +64,14 @@ func (c *Context) Switch(ctx string) error {
// KubeUpdate modifies kubeconfig default context. // KubeUpdate modifies kubeconfig default context.
func (c *Context) KubeUpdate(n string) error { func (c *Context) KubeUpdate(n string) error {
config, err := c.config().RawConfig() cfg := c.config().RawConfig()
if err != nil { if cfg == nil {
return err return errors.New("unable to fetch raw config")
} }
if err := c.Switch(n); err != nil { if err := c.Switch(n); err != nil {
return err return err
} }
return clientcmd.ModifyConfig( return clientcmd.ModifyConfig(
clientcmd.NewDefaultPathOptions(), config, true, clientcmd.NewDefaultPathOptions(), *cfg, true,
) )
} }

View File

@ -3,7 +3,7 @@ package dao
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -29,7 +29,7 @@ func TestCruiserSlice(t *testing.T) {
// Helpers... // Helpers...
func loadJSON(t assert.TestingT, n string) *unstructured.Unstructured { func loadJSON(t assert.TestingT, n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err) assert.Nil(t, err)
var o unstructured.Unstructured var o unstructured.Unstructured

View File

@ -3,7 +3,7 @@ package dao
import ( import (
"context" "context"
"errors" "errors"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@ -37,7 +37,7 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
return nil, errors.New("No dir in context") return nil, errors.New("No dir in context")
} }
files, err := ioutil.ReadDir(dir) files, err := os.ReadDir(dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,8 +48,8 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
continue continue
} }
oo = append(oo, render.DirRes{ oo = append(oo, render.DirRes{
Path: filepath.Join(dir, f.Name()), Path: filepath.Join(dir, f.Name()),
Info: f, Entry: f,
}) })
} }

View File

@ -69,20 +69,20 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
return err return err
} }
ns, _ := client.Namespaced(path) auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", []string{client.PatchVerb})
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }
if !auth { if !auth {
return fmt.Errorf("user is not authorized to restart a deployment") return fmt.Errorf("user is not authorized to restart a deployment")
} }
update, err := polymorphichelpers.ObjectRestarterFn(dp)
dial, err := d.Client().Dial()
if err != nil { if err != nil {
return err return err
} }
dial, err := d.Client().Dial() restarter, err := polymorphichelpers.ObjectRestarterFn(dp)
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +91,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
ctx, ctx,
dp.Name, dp.Name,
types.StrategicMergePatchType, types.StrategicMergePatchType,
update, restarter,
metav1.PatchOptions{}, metav1.PatchOptions{},
) )
return err return err

View File

@ -86,7 +86,7 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) e
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
} }
func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts *LogOptions) error { func podLogs(ctx context.Context, out LogChan, sel map[string]string, opts *LogOptions) error {
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
if !ok { if !ok {
return errors.New("expecting a context factory") return errors.New("expecting a context factory")
@ -110,14 +110,13 @@ func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts *LogOpt
po := Pod{} po := Pod{}
po.Init(f, client.NewGVR("v1/pods")) po.Init(f, client.NewGVR("v1/pods"))
for _, o := range oo { for _, o := range oo {
var pod v1.Pod u, ok := o.(*unstructured.Unstructured)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) if !ok {
if err != nil { return fmt.Errorf("expected unstructured got %t", o)
return err
} }
opts = opts.Clone() opts = opts.Clone()
opts.Path = client.FQN(pod.Namespace, pod.Name) opts.Path = client.FQN(u.GetNamespace(), u.GetName())
if err := po.TailLogs(ctx, c, opts); err != nil { if err := po.TailLogs(ctx, out, opts); err != nil {
return err return err
} }
} }

View File

@ -111,8 +111,7 @@ func (c *Helm) Delete(path string, cascade, force bool) error {
// EnsureHelmConfig return a new configuration. // EnsureHelmConfig return a new configuration.
func (c *Helm) EnsureHelmConfig(ns string) (*action.Configuration, error) { func (c *Helm) EnsureHelmConfig(ns string) (*action.Configuration, error) {
cfg := new(action.Configuration) cfg := new(action.Configuration)
flags := c.Client().Config().Flags() if err := cfg.Init(c.Client().Config().Flags(), ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
return nil, err return nil, err
} }
return cfg, nil return cfg, nil

View File

@ -2,39 +2,31 @@ package dao
import ( import (
"bytes" "bytes"
"fmt"
"regexp"
"time"
"github.com/derailed/k9s/internal/color"
) )
// LogChan represents a channel for logs. // LogChan represents a channel for logs.
type LogChan chan *LogItem type LogChan chan *LogItem
var ItemEOF = new(LogItem)
// LogItem represents a container log line. // LogItem represents a container log line.
type LogItem struct { type LogItem struct {
Pod, Container, Timestamp string Pod, Container string
SingleContainer bool SingleContainer bool
Bytes []byte Bytes []byte
} }
// NewLogItem returns a new item. // NewLogItem returns a new item.
func NewLogItem(b []byte) *LogItem { func NewLogItem(bb []byte) *LogItem {
space := []byte(" ")
cols := bytes.Split(b[:len(b)-1], space)
return &LogItem{ return &LogItem{
Timestamp: string(cols[0]), Bytes: bb,
Bytes: bytes.Join(cols[1:], space),
} }
} }
// NewLogItemFromString returns a new item. // NewLogItemFromString returns a new item.
func NewLogItemFromString(s string) *LogItem { func NewLogItemFromString(s string) *LogItem {
return &LogItem{ return &LogItem{
Bytes: []byte(s), Bytes: []byte(s),
Timestamp: time.Now().String(),
} }
} }
@ -46,22 +38,18 @@ func (l *LogItem) ID() string {
return l.Container return l.Container
} }
// Clone copies an item. // GetTimestamp fetch log lime timestamp
func (l *LogItem) Clone() *LogItem { func (l *LogItem) GetTimestamp() string {
bytes := make([]byte, len(l.Bytes)) index := bytes.Index(l.Bytes, []byte{' '})
copy(bytes, l.Bytes) if index < 0 {
return &LogItem{ return ""
Container: l.Container,
Pod: l.Pod,
Timestamp: l.Timestamp,
SingleContainer: l.SingleContainer,
Bytes: bytes,
} }
return string(l.Bytes[:index])
} }
// Info returns pod and container information. // Info returns pod and container information.
func (l *LogItem) Info() string { func (l *LogItem) Info() string {
return fmt.Sprintf("%q::%q", l.Pod, l.Container) return l.Pod + "::" + l.Container
} }
// IsEmpty checks if the entry is empty. // IsEmpty checks if the entry is empty.
@ -69,37 +57,39 @@ func (l *LogItem) IsEmpty() bool {
return len(l.Bytes) == 0 return len(l.Bytes) == 0
} }
var ( // Size returns the size of the item.
escPattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) func (l *LogItem) Size() int {
matcher = []byte("$1[]") return 100 + len(l.Bytes) + len(l.Pod) + len(l.Container)
) }
// Render returns a log line as string. // Render returns a log line as string.
func (l *LogItem) Render(paint int, showTime bool) []byte { func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) {
bb := make([]byte, 0, 200) index := bytes.Index(l.Bytes, []byte{' '})
if showTime { if showTime && index > 0 {
t := l.Timestamp bb.WriteString("[gray::]")
for i := len(t); i < 30; i++ { bb.Write(l.Bytes[:index])
t += " " bb.WriteString(" ")
for i := len(l.Bytes[:index]); i < 30; i++ {
bb.WriteByte(' ')
} }
bb = append(bb, color.ANSIColorize(t, 106)...)
bb = append(bb, ' ')
} }
var hasPod bool
if l.Pod != "" { if l.Pod != "" {
bb = append(bb, color.ANSIColorize(l.Pod, paint)...) bb.WriteString("[" + paint + "::]" + l.Pod)
hasPod = true
}
if !l.SingleContainer && l.Container != "" {
if hasPod {
bb = append(bb, ':')
}
bb = append(bb, color.ANSIColorize(l.Container, paint)...)
bb = append(bb, ' ')
} else if hasPod {
bb = append(bb, ' ')
} }
return append(bb, escPattern.ReplaceAll(l.Bytes, matcher)...) if !l.SingleContainer && l.Container != "" {
if len(l.Pod) > 0 {
bb.WriteString(" ")
}
bb.WriteString("[" + paint + "::b]" + l.Container + "[-::-] ")
} else if len(l.Pod) > 0 {
bb.WriteString("[-::]")
}
if index > 0 {
bb.Write(l.Bytes[index+1:])
} else {
bb.Write(l.Bytes)
}
} }

View File

@ -1,6 +1,7 @@
package dao_test package dao_test
import ( import (
"bytes"
"fmt" "fmt"
"testing" "testing"
@ -34,13 +35,13 @@ func TestLogItemRender(t *testing.T) {
}{ }{
"empty": { "empty": {
opts: dao.LogOptions{}, opts: dao.LogOptions{},
e: "Testing 1,2,3...", e: "Testing 1,2,3...\n",
}, },
"container": { "container": {
opts: dao.LogOptions{ opts: dao.LogOptions{
Container: "fred", Container: "fred",
}, },
e: "\x1b[38;5;0mfred\x1b[0m Testing 1,2,3...", e: "[yellow::b]fred[-::-] Testing 1,2,3...\n",
}, },
"pod": { "pod": {
opts: dao.LogOptions{ opts: dao.LogOptions{
@ -48,7 +49,7 @@ func TestLogItemRender(t *testing.T) {
Container: "blee", Container: "blee",
SingleContainer: true, SingleContainer: true,
}, },
e: "\x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...", e: "[yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n",
}, },
"full": { "full": {
opts: dao.LogOptions{ opts: dao.LogOptions{
@ -57,7 +58,7 @@ func TestLogItemRender(t *testing.T) {
SingleContainer: true, SingleContainer: true,
ShowTimestamp: true, ShowTimestamp: true,
}, },
e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00\x1b[0m \x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...", e: "[gray::]2018-12-14T10:36:43.326972-07:00 [yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n",
}, },
} }
@ -69,7 +70,9 @@ func TestLogItemRender(t *testing.T) {
_, n := client.Namespaced(u.opts.Path) _, n := client.Namespaced(u.opts.Path)
i.Pod, i.Container = n, u.opts.Container i.Pod, i.Container = n, u.opts.Container
assert.Equal(t, u.e, string(i.Render(0, u.opts.ShowTimestamp))) bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
i.Render("yellow", u.opts.ShowTimestamp, bb)
assert.Equal(t, u.e, bb.String())
}) })
} }
} }
@ -78,9 +81,24 @@ func BenchmarkLogItemRender(b *testing.B) {
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3...")) s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
i := dao.NewLogItem(s) i := dao.NewLogItem(s)
i.Pod, i.Container = "fred", "blee" i.Pod, i.Container = "fred", "blee"
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
i.Render(0, true) bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
i.Render("yellow", true, bb)
}
}
func BenchmarkLogItemRenderNoTS(b *testing.B) {
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
i := dao.NewLogItem(s)
i.Pod, i.Container = "fred", "blee"
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
i.Render("yellow", false, bb)
} }
} }

View File

@ -1,37 +1,37 @@
package dao package dao
import ( import (
"bytes"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"github.com/gdamore/tcell/v2"
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
) )
var colorPalette = []tcell.Color{ var podPalette = []string{
tcell.ColorTeal, "teal",
tcell.ColorGreen, "green",
tcell.ColorPurple, "purple",
tcell.ColorLime, "lime",
tcell.ColorBlue, "blue",
tcell.ColorYellow, "yellow",
tcell.ColorFuchsia, "fushia",
tcell.ColorAqua, "aqua",
} }
// LogItems represents a collection of log items. // LogItems represents a collection of log items.
type LogItems struct { type LogItems struct {
items []*LogItem items []*LogItem
colors map[string]tcell.Color podColors map[string]string
mx sync.RWMutex mx sync.RWMutex
} }
// NewLogItems returns a new instance. // NewLogItems returns a new instance.
func NewLogItems() *LogItems { func NewLogItems() *LogItems {
return &LogItems{ return &LogItems{
colors: make(map[string]tcell.Color), podColors: make(map[string]string),
} }
} }
@ -56,9 +56,9 @@ func (l *LogItems) Clear() {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
l.items = nil l.items = l.items[:0]
for k := range l.colors { for k := range l.podColors {
delete(l.colors, k) delete(l.podColors, k)
} }
} }
@ -76,8 +76,8 @@ func (l *LogItems) Subset(index int) *LogItems {
defer l.mx.RUnlock() defer l.mx.RUnlock()
return &LogItems{ return &LogItems{
items: l.items[index:], items: l.items[index:],
colors: l.colors, podColors: l.podColors,
} }
} }
@ -87,8 +87,8 @@ func (l *LogItems) Merge(n *LogItems) {
defer l.mx.Unlock() defer l.mx.Unlock()
l.items = append(l.items, n.items...) l.items = append(l.items, n.items...)
for k, v := range n.colors { for k, v := range n.podColors {
l.colors[k] = v l.podColors[k] = v
} }
} }
@ -101,47 +101,60 @@ func (l *LogItems) Add(ii ...*LogItem) {
} }
// Lines returns a collection of log lines. // Lines returns a collection of log lines.
func (l *LogItems) Lines(showTime bool) [][]byte { func (l *LogItems) Lines(index int, showTime bool, ll [][]byte) {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
ll := make([][]byte, len(l.items)) var colorIndex int
for i, item := range l.items { for i, item := range l.items[index:] {
color := l.colors[item.ID()] id := item.ID()
ll[i] = item.Render(int(color-tcell.ColorValid), showTime) color, ok := l.podColors[id]
if !ok {
if colorIndex >= len(podPalette) {
colorIndex = 0
}
color = podPalette[colorIndex]
l.podColors[id] = color
colorIndex++
}
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
item.Render(color, showTime, bb)
ll[i] = bb.Bytes()
} }
return ll
} }
// StrLines returns a collection of log lines. // StrLines returns a collection of log lines.
func (l *LogItems) StrLines(showTime bool) []string { func (l *LogItems) StrLines(index int, showTime bool) []string {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
ll := make([]string, len(l.items)) ll := make([]string, len(l.items[index:]))
for i, item := range l.items { for i, item := range l.items[index:] {
ll[i] = string(item.Render(0, showTime)) bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
item.Render("white", showTime, bb)
ll[i] = bb.String()
} }
return ll return ll
} }
// Render returns logs as a collection of strings. // Render returns logs as a collection of strings.
func (l *LogItems) Render(showTime bool, ll [][]byte) { func (l *LogItems) Render(index int, showTime bool, ll [][]byte) {
index := len(l.colors) var colorIndex int
for i, item := range l.items { for i, item := range l.items[index:] {
id := item.ID() id := item.ID()
color, ok := l.colors[id] color, ok := l.podColors[id]
if !ok { if !ok {
if index >= len(colorPalette) { if colorIndex >= len(podPalette) {
index = 0 colorIndex = 0
} }
color = colorPalette[index] color = podPalette[colorIndex]
l.colors[id] = color l.podColors[id] = color
index++ colorIndex++
} }
ll[i] = item.Render(int(color-tcell.ColorValid), showTime) bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
item.Render(color, showTime, bb)
ll[i] = bb.Bytes()
} }
} }
@ -154,15 +167,15 @@ func (l *LogItems) DumpDebug(m string) {
} }
// Filter filters out log items based on given filter. // Filter filters out log items based on given filter.
func (l *LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) { func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, error) {
if q == "" { if q == "" {
return nil, nil, nil return nil, nil, nil
} }
if IsFuzzySelector(q) { if IsFuzzySelector(q) {
mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:]), showTime) mm, ii := l.fuzzyFilter(index, strings.TrimSpace(q[2:]), showTime)
return mm, ii, nil return mm, ii, nil
} }
matches, indices, err := l.filterLogs(q, showTime) matches, indices, err := l.filterLogs(index, q, showTime)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -170,10 +183,10 @@ func (l *LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) {
return matches, indices, nil return matches, indices, nil
} }
func (l *LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) { func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]int) {
q = strings.TrimSpace(q) q = strings.TrimSpace(q)
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10) matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10)
mm := fuzzy.Find(q, l.StrLines(showTime)) mm := fuzzy.Find(q, l.StrLines(index, showTime))
for _, m := range mm { for _, m := range mm {
matches = append(matches, m.Index) matches = append(matches, m.Index)
indices = append(indices, m.MatchedIndexes) indices = append(indices, m.MatchedIndexes)
@ -182,7 +195,7 @@ func (l *LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
return matches, indices return matches, indices
} }
func (l *LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) { func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]int, error) {
var invert bool var invert bool
if IsInverseSelector(q) { if IsInverseSelector(q) {
invert = true invert = true
@ -193,7 +206,9 @@ func (l *LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
return nil, nil, err return nil, nil, err
} }
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10) matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10)
for i, line := range l.Lines(showTime) { ll := make([][]byte, len(l.items[index:]))
l.Lines(index, showTime, ll)
for i, line := range ll {
locs := rx.FindIndex(line) locs := rx.FindIndex(line)
if locs != nil && invert { if locs != nil && invert {
continue continue

View File

@ -71,7 +71,7 @@ func TestLogItemsFilter(t *testing.T) {
for _, i := range ii.Items() { for _, i := range ii.Items() {
i.Pod, i.Container = n, u.opts.Container i.Pod, i.Container = n, u.opts.Container
} }
res, _, err := ii.Filter(u.q, false) res, _, err := ii.Filter(0, u.q, false)
assert.Equal(t, u.err, err) assert.Equal(t, u.err, err)
if err == nil { if err == nil {
assert.Equal(t, u.e, res) assert.Equal(t, u.e, res)
@ -87,20 +87,20 @@ func TestLogItemsRender(t *testing.T) {
}{ }{
"empty": { "empty": {
opts: dao.LogOptions{}, opts: dao.LogOptions{},
e: "Testing 1,2,3...", e: "Testing 1,2,3...\n",
}, },
"container": { "container": {
opts: dao.LogOptions{ opts: dao.LogOptions{
Container: "fred", Container: "fred",
}, },
e: "\x1b[38;5;6mfred\x1b[0m Testing 1,2,3...", e: "[teal::b]fred[-::-] Testing 1,2,3...\n",
}, },
"pod": { "pod-container": {
opts: dao.LogOptions{ opts: dao.LogOptions{
Path: "blee/fred", Path: "blee/fred",
Container: "blee", Container: "blee",
}, },
e: "\x1b[38;5;6mfred\x1b[0m:\x1b[38;5;6mblee\x1b[0m Testing 1,2,3...", e: "[teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n",
}, },
"full": { "full": {
opts: dao.LogOptions{ opts: dao.LogOptions{
@ -108,7 +108,7 @@ func TestLogItemsRender(t *testing.T) {
Container: "blee", Container: "blee",
ShowTimestamp: true, ShowTimestamp: true,
}, },
e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00\x1b[0m \x1b[38;5;6mfred\x1b[0m:\x1b[38;5;6mblee\x1b[0m Testing 1,2,3...", e: "[gray::]2018-12-14T10:36:43.326972-07:00 [teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n",
}, },
} }
@ -121,7 +121,7 @@ func TestLogItemsRender(t *testing.T) {
ii.Items()[0].Pod, ii.Items()[0].Container = n, u.opts.Container ii.Items()[0].Pod, ii.Items()[0].Container = n, u.opts.Container
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
res := make([][]byte, 1) res := make([][]byte, 1)
ii.Render(u.opts.ShowTimestamp, res) ii.Render(0, u.opts.ShowTimestamp, res)
assert.Equal(t, u.e, string(res[0])) assert.Equal(t, u.e, string(res[0]))
}) })
} }

View File

@ -2,7 +2,6 @@ package dao
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -12,12 +11,14 @@ import (
// LogOptions represents logger options. // LogOptions represents logger options.
type LogOptions struct { type LogOptions struct {
CreateDuration time.Duration
Path string Path string
Container string Container string
DefaultContainer string DefaultContainer string
SinceTime string SinceTime string
Lines int64 Lines int64
SinceSeconds int64 SinceSeconds int64
Head bool
Previous bool Previous bool
SingleContainer bool SingleContainer bool
MultiPods bool MultiPods bool
@ -77,6 +78,18 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
Previous: o.Previous, Previous: o.Previous,
TailLines: &o.Lines, TailLines: &o.Lines,
} }
if o.Head {
var maxBytes int64 = 1000
//var defaultTail int64 = -1
//var defaultSince int64
opts.Follow = false
opts.TailLines, opts.SinceSeconds, opts.SinceTime = nil, nil, nil
//opts.TailLines = &defaultTail
//opts.SinceSeconds = &defaultSince
opts.LimitBytes = &maxBytes
return &opts
}
if o.SinceSeconds < 0 { if o.SinceSeconds < 0 {
return &opts return &opts
} }
@ -96,21 +109,6 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
return &opts return &opts
} }
// FixedSizeName returns a normalize fixed size pod name if possible.
func (o *LogOptions) FixedSizeName() string {
_, n := client.Namespaced(o.Path)
tokens := strings.Split(n, "-")
if len(tokens) < 3 {
return n
}
var s []string
for i := 0; i < len(tokens)-1; i++ {
s = append(s, tokens[i])
}
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
}
// DecorateLog add a log header to display po/co information along with the log message. // DecorateLog add a log header to display po/co information along with the log message.
func (o *LogOptions) DecorateLog(bytes []byte) *LogItem { func (o *LogOptions) DecorateLog(bytes []byte) *LogItem {
item := NewLogItem(bytes) item := NewLogItem(bytes)

View File

@ -8,7 +8,7 @@ package dao
// "encoding/json" // "encoding/json"
// "errors" // "errors"
// "fmt" // "fmt"
// "io/ioutil" // "io"
// "net/http" // "net/http"
// "net/url" // "net/url"
// "os" // "os"
@ -193,7 +193,7 @@ package dao
// case http.StatusUnauthorized: // case http.StatusUnauthorized:
// return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server") // return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server")
// default: // default:
// bytesOut, err := ioutil.ReadAll(delRes.Body) // bytesOut, err := io.ReadAll(delRes.Body)
// if err != nil { // if err != nil {
// return err // return err
// } // }

View File

@ -177,7 +177,7 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
} }
// TailLogs tails a given container logs. // TailLogs tails a given container logs.
func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error { func (p *Pod) TailLogs(ctx context.Context, out LogChan, opts *LogOptions) error {
log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container) log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container)
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
if !ok { if !ok {
@ -198,18 +198,18 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
if co, ok := GetDefaultLogContainer(po.ObjectMeta, po.Spec); ok && !opts.AllContainers { if co, ok := GetDefaultLogContainer(po.ObjectMeta, po.Spec); ok && !opts.AllContainers {
opts.DefaultContainer = co opts.DefaultContainer = co
return tailLogs(ctx, p, c, opts) return tailLogs(ctx, p, out, opts)
} }
if opts.HasContainer() && !opts.AllContainers { if opts.HasContainer() && !opts.AllContainers {
return tailLogs(ctx, p, c, opts) return tailLogs(ctx, p, out, opts)
} }
var tailed bool var tailed bool
for _, co := range po.Spec.InitContainers { for _, co := range po.Spec.InitContainers {
o := opts.Clone() o := opts.Clone()
o.Container = co.Name o.Container = co.Name
if err := tailLogs(ctx, p, c, o); err != nil { if err := tailLogs(ctx, p, out, o); err != nil {
return err return err
} }
tailed = true tailed = true
@ -217,7 +217,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
for _, co := range po.Spec.Containers { for _, co := range po.Spec.Containers {
o := opts.Clone() o := opts.Clone()
o.Container = co.Name o.Container = co.Name
if err := tailLogs(ctx, p, c, o); err != nil { if err := tailLogs(ctx, p, out, o); err != nil {
return err return err
} }
tailed = true tailed = true
@ -225,7 +225,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
for _, co := range po.Spec.EphemeralContainers { for _, co := range po.Spec.EphemeralContainers {
o := opts.Clone() o := opts.Clone()
o.Container = co.Name o.Container = co.Name
if err := tailLogs(ctx, p, c, o); err != nil { if err := tailLogs(ctx, p, out, o); err != nil {
return err return err
} }
tailed = true tailed = true
@ -326,21 +326,22 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func tailLogs(ctx context.Context, logger Logger, c LogChan, opts *LogOptions) error { func tailLogs(ctx context.Context, logger Logger, out LogChan, opts *LogOptions) error {
log.Debug().Msgf("Tailing logs for %#v", opts)
var ( var (
err error err error
req *restclient.Request req *restclient.Request
stream io.ReadCloser stream io.ReadCloser
) )
o := opts.ToPodLogOptions()
log.Debug().Msgf("TAIL_LOGS! %#v", o)
done: done:
for r := 0; r < logRetryCount; r++ { for r := 0; r < logRetryCount; r++ {
req, err = logger.Logs(opts.Path, opts.ToPodLogOptions()) req, err = logger.Logs(opts.Path, o)
if err == nil { if err == nil {
// This call will block if nothing is in the stream!! // This call will block if nothing is in the stream!!
if stream, err = req.Stream(ctx); err == nil { if stream, err = req.Stream(ctx); err == nil {
go readLogs(stream, c, opts) go readLogs(ctx, stream, out, opts)
break break
} else { } else {
log.Error().Err(err).Msg("Streaming logs") log.Error().Err(err).Msg("Streaming logs")
@ -351,6 +352,7 @@ done:
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msgf("!!!!TAIL_LOGS CANCELED!!!!")
err = ctx.Err() err = ctx.Err()
break done break done
default: default:
@ -358,36 +360,36 @@ done:
} }
} }
if err != nil { return err
log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path)
c <- opts.DecorateLog([]byte("\n" + err.Error() + "\n"))
return err
}
return nil
} }
func readLogs(stream io.ReadCloser, c LogChan, opts *LogOptions) { func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOptions) {
defer func() { defer func() {
log.Debug().Msgf(">>> Closing stream %s", opts.Info()) log.Debug().Msgf("READ_LOGS BAILED!!!")
if err := stream.Close(); err != nil { if err := stream.Close(); err != nil {
log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info()) log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info())
} }
}() }()
log.Debug().Msgf("READ_LOGS PROCESSING %#v", opts)
r := bufio.NewReader(stream) r := bufio.NewReader(stream)
for { for {
bytes, err := r.ReadBytes('\n') bytes, err := r.ReadBytes('\n')
if err != nil { if err != nil {
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info()) log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info())
// c <- ItemEOF
return return
} }
log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info()) log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info())
c <- opts.DecorateLog([]byte(fmt.Sprintf("\nlog stream failed: %#v\n", err)))
return return
} }
c <- opts.DecorateLog(bytes) select {
case c <- opts.DecorateLog(bytes):
case <-ctx.Done():
log.Debug().Msgf("READER CANCELED")
return
}
} }
} }

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/port"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -28,8 +29,7 @@ type PortForwarder struct {
stopChan, readyChan chan struct{} stopChan, readyChan chan struct{}
active bool active bool
path string path string
container string tunnel port.PortTunnel
ports []string
age time.Time age time.Time
} }
@ -57,58 +57,56 @@ func (p *PortForwarder) SetActive(b bool) {
p.active = b p.active = b
} }
// Ports returns the forwarded ports mappings. // Port returns the port mapping.
func (p *PortForwarder) Ports() []string { func (p *PortForwarder) Port() string {
return p.ports return p.tunnel.PortMap()
}
// ContainerPort returns the container port.
func (p *PortForwarder) ContainerPort() string {
return p.tunnel.ContainerPort
}
// LocalPort returns the local port.
func (p *PortForwarder) LocalPort() string {
return p.tunnel.LocalPort
} }
// Path returns the pod resource path. // Path returns the pod resource path.
func (p *PortForwarder) Path() string { func (p *PortForwarder) Path() string {
return PortForwardID(p.path, p.container) return PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap())
} }
// PortForwardID computes port-forward identifier. // ID returns a pf id.
func PortForwardID(path, co string) string { func (p *PortForwarder) ID() string {
return path + ":" + co return PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap())
} }
// Container returns the target's container. // Container returns the target's container.
func (p *PortForwarder) Container() string { func (p *PortForwarder) Container() string {
return p.container return p.tunnel.Container
} }
// Stop terminates a port forward. // Stop terminates a port forward.
func (p *PortForwarder) Stop() { func (p *PortForwarder) Stop() {
log.Debug().Msgf("<<< Stopping PortForward %q %v", p.path, p.ports) log.Debug().Msgf("<<< Stopping PortForward %s", p.ID())
p.active = false p.active = false
close(p.stopChan) close(p.stopChan)
} }
// FQN returns the portforward unique id. // FQN returns the portforward unique id.
func (p *PortForwarder) FQN() string { func (p *PortForwarder) FQN() string {
return p.path + ":" + p.container return p.path + ":" + p.tunnel.Container
} }
// HasPortMapping checks if port mapping is defined for this fwd. // HasPortMapping checks if port mapping is defined for this fwd.
func (p *PortForwarder) HasPortMapping(m string) bool { func (p *PortForwarder) HasPortMapping(portMap string) bool {
for _, mapping := range p.ports { return p.tunnel.PortMap() == portMap
if mapping == m {
return true
}
}
return false
} }
// Start initiates a port forward session for a given pod and ports. // Start initiates a port forward session for a given pod and ports.
func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) { func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.PortForwarder, error) {
if len(tt) == 0 { p.path, p.tunnel, p.age = path, tt, time.Now()
return nil, fmt.Errorf("no ports assigned")
}
fwds, addrs := make([]string, 0, len(tt)), make([]string, 0, len(tt))
for _, t := range tt {
fwds, addrs = append(fwds, t.PortMap()), append(addrs, t.Address)
}
p.path, p.container, p.ports, p.age = path, co, fwds, time.Now()
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb}) auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb})
@ -155,10 +153,10 @@ func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portfor
Name(n). Name(n).
SubResource("portforward") SubResource("portforward")
return p.forwardPorts("POST", req.URL(), addrs, fwds) return p.forwardPorts("POST", req.URL(), tt.Address, tt.PortMap())
} }
func (p *PortForwarder) forwardPorts(method string, url *url.URL, addrs, ports []string) (*portforward.PortForwarder, error) { func (p *PortForwarder) forwardPorts(method string, url *url.URL, addr, portMap string) (*portforward.PortForwarder, error) {
cfg, err := p.Client().Config().RESTConfig() cfg, err := p.Client().Config().RESTConfig()
if err != nil { if err != nil {
return nil, err return nil, err
@ -169,12 +167,17 @@ func (p *PortForwarder) forwardPorts(method string, url *url.URL, addrs, ports [
} }
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
return portforward.NewOnAddresses(dialer, addrs, ports, p.stopChan, p.readyChan, p.Out, p.ErrOut) return portforward.NewOnAddresses(dialer, []string{addr}, []string{portMap}, p.stopChan, p.readyChan, p.Out, p.ErrOut)
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
// PortForwardID computes port-forward identifier.
func PortForwardID(path, co, portMap string) string {
return path + "|" + co + "|" + portMap
}
func codec() (serializer.CodecFactory, runtime.ParameterCodec) { func codec() (serializer.CodecFactory, runtime.ParameterCodec) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
gv := schema.GroupVersion{Group: "", Version: "v1"} gv := schema.GroupVersion{Group: "", Version: "v1"}

View File

@ -341,7 +341,7 @@ func loadRBAC(m ResourceMetas) {
func loadPreferred(f Factory, m ResourceMetas) error { func loadPreferred(f Factory, m ResourceMetas) error {
if !f.Client().ConnectionOK() { if !f.Client().ConnectionOK() {
log.Error().Msgf("PreferredRES - No API server connection") log.Error().Msgf("Load cluster resources - No API server connection")
return nil return nil
} }
@ -393,6 +393,9 @@ func isDeprecated(gvr client.GVR) bool {
} }
func loadCRDs(f Factory, m ResourceMetas) { func loadCRDs(f Factory, m ResourceMetas) {
if !f.Client().ConnectionOK() {
return
}
const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions" const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything()) oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
if err != nil { if err != nil {

View File

@ -3,7 +3,7 @@ package dao
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -89,7 +89,7 @@ func TestExtractString(t *testing.T) {
// Helpers... // Helpers...
func load(t *testing.T, n string) *unstructured.Unstructured { func load(t *testing.T, n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err) assert.Nil(t, err)
var o unstructured.Unstructured var o unstructured.Unstructured

View File

@ -3,7 +3,6 @@ package dao
import ( import (
"context" "context"
"errors" "errors"
"io/ioutil"
"os" "os"
"regexp" "regexp"
@ -37,14 +36,16 @@ func (d *ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, erro
return nil, errors.New("no screendump dir found in context") return nil, errors.New("no screendump dir found in context")
} }
ff, err := ioutil.ReadDir(SanitizeFilename(dir)) ff, err := os.ReadDir(SanitizeFilename(dir))
if err != nil { if err != nil {
return nil, err return nil, err
} }
oo := make([]runtime.Object, len(ff)) oo := make([]runtime.Object, len(ff))
for i, f := range ff { for i, f := range ff {
oo[i] = render.FileRes{File: f, Dir: dir} if fi, err := f.Info(); err == nil {
oo[i] = render.FileRes{File: fi, Dir: dir}
}
} }
return oo, nil return oo, nil

View File

@ -70,8 +70,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
return err return err
} }
ns, _ := client.Namespaced(path) auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", []string{client.PatchVerb})
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets", []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }
@ -95,6 +94,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
update, update,
metav1.PatchOptions{}, metav1.PatchOptions{},
) )
return err return err
} }

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil" "io"
"net/http" "net/http"
"time" "time"
@ -55,13 +55,7 @@ func NewClusterMeta() ClusterMeta {
// Deltas diffs cluster meta return true if different, false otherwise. // Deltas diffs cluster meta return true if different, false otherwise.
func (c ClusterMeta) Deltas(n ClusterMeta) bool { func (c ClusterMeta) Deltas(n ClusterMeta) bool {
if c.Cpu != n.Cpu { if c.Cpu != n.Cpu || c.Mem != n.Mem || c.Ephemeral != n.Ephemeral {
return true
}
if c.Mem != n.Mem {
return true
}
if c.Ephemeral != n.Ephemeral {
return true return true
} }
@ -76,6 +70,7 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool {
// ClusterInfo models cluster metadata. // ClusterInfo models cluster metadata.
type ClusterInfo struct { type ClusterInfo struct {
cluster *Cluster cluster *Cluster
factory dao.Factory
data ClusterMeta data ClusterMeta
version string version string
listeners []ClusterInfoListener listeners []ClusterInfoListener
@ -83,11 +78,12 @@ type ClusterInfo struct {
} }
// NewClusterInfo returns a new instance. // NewClusterInfo returns a new instance.
func NewClusterInfo(f dao.Factory, version string) *ClusterInfo { func NewClusterInfo(f dao.Factory, v string) *ClusterInfo {
c := ClusterInfo{ c := ClusterInfo{
factory: f,
cluster: NewCluster(f), cluster: NewCluster(f),
data: NewClusterMeta(), data: NewClusterMeta(),
version: version, version: v,
cache: cache.NewLRUExpireCache(cacheSize), cache: cache.NewLRUExpireCache(cacheSize),
} }
@ -116,28 +112,29 @@ func (c *ClusterInfo) Reset(f dao.Factory) {
c.Refresh() c.Refresh()
} }
// Refresh fetches latest cluster meta. // Refresh fetches the latest cluster meta.
func (c *ClusterInfo) Refresh() { func (c *ClusterInfo) Refresh() {
data := NewClusterMeta() data := NewClusterMeta()
data.Context = c.cluster.ContextName() if c.factory.Client().ConnectionOK() {
data.Cluster = c.cluster.ClusterName() data.Context = c.cluster.ContextName()
data.User = c.cluster.UserName() data.Cluster = c.cluster.ClusterName()
data.User = c.cluster.UserName()
data.K8sVer = c.cluster.Version()
ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout())
defer cancel()
var mx client.ClusterMetrics
if err := c.cluster.Metrics(ctx, &mx); err == nil {
data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral
} else {
log.Warn().Err(err).Msgf("Cluster metrics failed")
}
}
data.K9sVer = c.version data.K9sVer = c.version
v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev()) v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev())
data.K9sVer, data.K9sLatest = v1.String(), v2.String() data.K9sVer, data.K9sLatest = v1.String(), v2.String()
if v1.IsCurrent(v2) { if v1.IsCurrent(v2) {
data.K9sLatest = "" data.K9sLatest = ""
} }
data.K8sVer = c.cluster.Version()
ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout())
defer cancel()
var mx client.ClusterMetrics
if err := c.cluster.Metrics(ctx, &mx); err == nil {
data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral
} else {
log.Warn().Err(err).Msgf("Cluster metrics failed")
}
if c.data.Deltas(data) { if c.data.Deltas(data) {
c.fireMetaChanged(c.data, data) c.fireMetaChanged(c.data, data)
@ -200,7 +197,7 @@ func fetchLatestRev() (string, error) {
} }
}() }()
b, err := ioutil.ReadAll(resp.Body) b, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -91,8 +91,8 @@ func (c *CmdBuff) Add(r rune) {
if c.cancel != nil { if c.cancel != nil {
return return
} }
var ctx context.Context ctx := context.Background()
ctx, c.cancel = context.WithTimeout(context.Background(), keyEntryDelay) ctx, c.cancel = context.WithTimeout(ctx, keyEntryDelay)
go func() { go func() {
<-ctx.Done() <-ctx.Done()
@ -118,8 +118,8 @@ func (c *CmdBuff) Delete() {
return return
} }
var ctx context.Context ctx := context.Background()
ctx, c.cancel = context.WithTimeout(context.Background(), 800*time.Millisecond) ctx, c.cancel = context.WithTimeout(ctx, 800*time.Millisecond)
go func() { go func() {
<-ctx.Done() <-ctx.Done()

View File

@ -126,8 +126,8 @@ func (f *Flash) SetMessage(level FlashLevel, msg string) {
f.setLevelMessage(LevelMessage{Level: level, Text: msg}) f.setLevelMessage(LevelMessage{Level: level, Text: msg})
f.fireFlashChanged() f.fireFlashChanged()
var ctx context.Context ctx := context.Background()
ctx, f.cancel = context.WithCancel(context.Background()) ctx, f.cancel = context.WithCancel(ctx)
go f.refresh(ctx) go f.refresh(ctx)
} }

View File

@ -24,12 +24,21 @@ type LogsListener interface {
// LogFailed indicates a log failure. // LogFailed indicates a log failure.
LogFailed(error) LogFailed(error)
// LogStop indicates logging was canceled.
LogStop()
// LogResume indicates loggings has resumed.
LogResume()
// LogCanceled indicates no more logs will come.
LogCanceled()
} }
// Log represents a resource logger. // Log represents a resource logger.
type Log struct { type Log struct {
factory dao.Factory factory dao.Factory
items *dao.LogItems lines *dao.LogItems
listeners []LogsListener listeners []LogsListener
gvr client.GVR gvr client.GVR
logOptions *dao.LogOptions logOptions *dao.LogOptions
@ -45,11 +54,19 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *L
return &Log{ return &Log{
gvr: gvr, gvr: gvr,
logOptions: opts, logOptions: opts,
items: dao.NewLogItems(), lines: dao.NewLogItems(),
flushTimeout: flushTimeout, flushTimeout: flushTimeout,
} }
} }
func (l *Log) GVR() client.GVR {
return l.gvr
}
func (l *Log) LogOptions() *dao.LogOptions {
return l.logOptions
}
// SinceSeconds returns since seconds option. // SinceSeconds returns since seconds option.
func (l *Log) SinceSeconds() int64 { func (l *Log) SinceSeconds() int64 {
l.mx.RLock() l.mx.RLock()
@ -58,16 +75,33 @@ func (l *Log) SinceSeconds() int64 {
return l.logOptions.SinceSeconds return l.logOptions.SinceSeconds
} }
// IsHead returns log head option.
func (l *Log) IsHead() bool {
l.mx.RLock()
defer l.mx.RUnlock()
return l.logOptions.Head
}
// ToggleShowTimestamp toggles to logs timestamps. // ToggleShowTimestamp toggles to logs timestamps.
func (l *Log) ToggleShowTimestamp(b bool) { func (l *Log) ToggleShowTimestamp(b bool) {
l.logOptions.ShowTimestamp = b l.logOptions.ShowTimestamp = b
l.Refresh() l.Refresh()
} }
func (l *Log) Head(ctx context.Context, c dao.LogChan) {
l.mx.Lock()
{
l.logOptions.Head = true
}
l.mx.Unlock()
l.Restart(ctx, c, true)
}
// SetSinceSeconds sets the logs retrieval time. // SetSinceSeconds sets the logs retrieval time.
func (l *Log) SetSinceSeconds(i int64) { func (l *Log) SetSinceSeconds(ctx context.Context, c dao.LogChan, i int64) {
l.logOptions.SinceSeconds = i l.logOptions.SinceSeconds, l.logOptions.Head = i, false
l.Restart() l.Restart(ctx, c, true)
} }
// Configure sets logger configuration. // Configure sets logger configuration.
@ -100,7 +134,7 @@ func (l *Log) Init(f dao.Factory) {
func (l *Log) Clear() { func (l *Log) Clear() {
l.mx.Lock() l.mx.Lock()
{ {
l.items.Clear() l.lines.Clear()
l.lastSent = 0 l.lastSent = 0
} }
l.mx.Unlock() l.mx.Unlock()
@ -111,21 +145,24 @@ func (l *Log) Clear() {
// Refresh refreshes the logs. // Refresh refreshes the logs.
func (l *Log) Refresh() { func (l *Log) Refresh() {
l.fireLogCleared() l.fireLogCleared()
ll := make([][]byte, l.items.Len()) ll := make([][]byte, l.lines.Len())
l.items.Render(l.logOptions.ShowTimestamp, ll) l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
l.fireLogChanged(ll) l.fireLogChanged(ll)
} }
// Restart restarts the logger. // Restart restarts the logger.
func (l *Log) Restart() { func (l *Log) Restart(ctx context.Context, c dao.LogChan, clear bool) {
l.Clear()
l.Stop() l.Stop()
l.Start() if clear {
l.Clear()
}
l.fireLogResume()
l.Start(ctx, c)
} }
// Start starts logging. // Start starts logging.
func (l *Log) Start() { func (l *Log) Start(ctx context.Context, c dao.LogChan) {
if err := l.load(); err != nil { if err := l.load(ctx, c); err != nil {
log.Error().Err(err).Msgf("Tail logs failed!") log.Error().Err(err).Msgf("Tail logs failed!")
l.fireLogError(err) l.fireLogError(err)
} }
@ -134,23 +171,20 @@ func (l *Log) Start() {
// Stop terminates logging. // Stop terminates logging.
func (l *Log) Stop() { func (l *Log) Stop() {
defer log.Debug().Msgf("<<<< Logger STOPPED!") defer log.Debug().Msgf("<<<< Logger STOPPED!")
if l.cancelFn != nil { l.cancel()
l.cancelFn()
l.cancelFn = nil
}
} }
// Set sets the log lines (for testing only!) // Set sets the log lines (for testing only!)
func (l *Log) Set(items *dao.LogItems) { func (l *Log) Set(lines *dao.LogItems) {
l.mx.Lock() l.mx.Lock()
{ {
l.items.Merge(items) l.lines.Merge(lines)
} }
l.mx.Unlock() l.mx.Unlock()
l.fireLogCleared() l.fireLogCleared()
ll := make([][]byte, l.items.Len()) ll := make([][]byte, l.lines.Len())
l.items.Render(l.logOptions.ShowTimestamp, ll) l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
l.fireLogChanged(ll) l.fireLogChanged(ll)
} }
@ -163,34 +197,31 @@ func (l *Log) ClearFilter() {
l.mx.Unlock() l.mx.Unlock()
l.fireLogCleared() l.fireLogCleared()
ll := make([][]byte, l.items.Len()) ll := make([][]byte, l.lines.Len())
l.items.Render(l.logOptions.ShowTimestamp, ll) l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
l.fireLogChanged(ll) l.fireLogChanged(ll)
} }
// Filter filters the model using either fuzzy or regexp. // Filter filters the model using either fuzzy or regexp.
func (l *Log) Filter(q string) { func (l *Log) Filter(q string) {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() {
l.filter = q
if len(q) == 0 {
l.filter = ""
l.fireLogCleared()
l.fireLogBuffChanged(l.items)
return
} }
l.mx.Unlock()
l.filter = q
l.fireLogCleared() l.fireLogCleared()
l.fireLogBuffChanged(l.items) l.fireLogBuffChanged(0)
} }
func (l *Log) load() error { func (l *Log) load(ctx context.Context, c dao.LogChan) error {
var ctx context.Context if l.cancelFn != nil {
l.cancelFn()
l.cancelFn = nil
}
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory) ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
ctx, l.cancelFn = context.WithCancel(ctx) ctx, l.cancelFn = context.WithCancel(ctx)
c := make(dao.LogChan, 10)
go l.updateLogs(ctx, c) go l.updateLogs(ctx, c)
accessor, err := dao.AccessorFor(l.factory, l.gvr) accessor, err := dao.AccessorFor(l.factory, l.gvr)
@ -205,40 +236,40 @@ func (l *Log) load() error {
go func() { go func() {
if err = loggable.TailLogs(ctx, c, l.logOptions); err != nil { if err = loggable.TailLogs(ctx, c, l.logOptions); err != nil {
log.Error().Err(err).Msgf("Tail logs failed") log.Error().Err(err).Msgf("Tail logs failed")
l.mx.Lock() l.cancel()
if l.cancelFn != nil {
l.cancelFn()
}
l.mx.Unlock()
} }
}() }()
return nil return nil
} }
func (l *Log) cancel() {
l.mx.Lock()
{
if l.cancelFn == nil {
l.mx.Unlock()
return
}
l.cancelFn()
l.cancelFn = nil
}
l.mx.Unlock()
}
// Append adds a log line. // Append adds a log line.
func (l *Log) Append(line *dao.LogItem) { func (l *Log) Append(line *dao.LogItem) {
if line == nil || line.IsEmpty() { if line == nil || line.IsEmpty() {
return return
} }
l.mx.Lock()
{
l.logOptions.SinceTime = line.Timestamp
}
l.mx.Unlock()
if l.items.Len() == 0 {
l.fireLogCleared()
}
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
if l.items.Len() < int(l.logOptions.Lines) { l.logOptions.SinceTime = line.GetTimestamp()
l.items.Add(line) if l.lines.Len() < int(l.logOptions.Lines) {
l.lines.Add(line)
return return
} }
l.items.Shift(line) l.lines.Shift(line)
l.lastSent-- l.lastSent--
if l.lastSent < 0 { if l.lastSent < 0 {
l.lastSent = 0 l.lastSent = 0
@ -250,36 +281,40 @@ func (l *Log) Notify() {
l.mx.Lock() l.mx.Lock()
defer l.mx.Unlock() defer l.mx.Unlock()
if l.lastSent < l.items.Len() { if l.lastSent < l.lines.Len() {
l.fireLogBuffChanged(l.items.Subset(l.lastSent)) l.fireLogBuffChanged(l.lastSent)
l.lastSent = l.items.Len() l.lastSent = l.lines.Len()
} }
} }
// ToggleAllContainers toggles to show all containers logs. // ToggleAllContainers toggles to show all containers logs.
func (l *Log) ToggleAllContainers() { func (l *Log) ToggleAllContainers(ctx context.Context, c dao.LogChan) {
l.logOptions.ToggleAllContainers() l.logOptions.ToggleAllContainers()
l.Restart() l.Restart(ctx, c, true)
} }
func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) { func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
defer func() { defer log.Debug().Msgf("updateLogs view bailing out!")
log.Debug().Msgf("updateLogs view bailing out!")
}()
for { for {
select { select {
case item, ok := <-c: case item, ok := <-c:
if !ok { if !ok {
log.Debug().Msgf("Closed channel detected. Bailing out...") log.Debug().Msgf("Closed channel detected. Bailing out!")
l.Append(item) l.Append(item)
l.Notify() l.Notify()
return return
} }
if item == dao.ItemEOF {
log.Debug().Msgf("!!!!!GOT EOF!!!!!!")
l.fireCanceled()
return
}
l.Append(item) l.Append(item)
var overflow bool var overflow bool
l.mx.RLock() l.mx.RLock()
{ {
overflow = int64(l.items.Len()-l.lastSent) > l.logOptions.Lines overflow = int64(l.lines.Len()-l.lastSent) > l.logOptions.Lines
} }
l.mx.RUnlock() l.mx.RUnlock()
if overflow { if overflow {
@ -288,6 +323,7 @@ func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
case <-time.After(l.flushTimeout): case <-time.After(l.flushTimeout):
l.Notify() l.Notify()
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msgf("!!!LOG_MODEL IS CANCELED!!!")
return return
} }
} }
@ -295,11 +331,17 @@ func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
// AddListener adds a new model listener. // AddListener adds a new model listener.
func (l *Log) AddListener(listener LogsListener) { func (l *Log) AddListener(listener LogsListener) {
l.mx.Lock()
defer l.mx.Unlock()
l.listeners = append(l.listeners, listener) l.listeners = append(l.listeners, listener)
} }
// RemoveListener delete a listener from the list. // RemoveListener delete a listener from the list.
func (l *Log) RemoveListener(listener LogsListener) { func (l *Log) RemoveListener(listener LogsListener) {
l.mx.Lock()
defer l.mx.Unlock()
victim := -1 victim := -1
for i, lis := range l.listeners { for i, lis := range l.listeners {
if lis == listener { if lis == listener {
@ -313,19 +355,19 @@ func (l *Log) RemoveListener(listener LogsListener) {
} }
} }
func (l *Log) applyFilter(q string) ([][]byte, error) { func (l *Log) applyFilter(index int, q string) ([][]byte, error) {
if q == "" { if q == "" {
return nil, nil return nil, nil
} }
matches, indices, err := l.items.Filter(q, l.logOptions.ShowTimestamp) matches, indices, err := l.lines.Filter(index, q, l.logOptions.ShowTimestamp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// No filter! // No filter!
if matches == nil { if matches == nil {
ll := make([][]byte, l.items.Len()) ll := make([][]byte, l.lines.Len())
l.items.Render(l.logOptions.ShowTimestamp, ll) l.lines.Render(index, l.logOptions.ShowTimestamp, ll)
return ll, nil return ll, nil
} }
// Blank filter // Blank filter
@ -333,31 +375,45 @@ func (l *Log) applyFilter(q string) ([][]byte, error) {
return nil, nil return nil, nil
} }
filtered := make([][]byte, 0, len(matches)) filtered := make([][]byte, 0, len(matches))
lines := l.items.Lines(l.logOptions.ShowTimestamp) ll := make([][]byte, l.lines.Len())
l.lines.Lines(index, l.logOptions.ShowTimestamp, ll)
for i, idx := range matches { for i, idx := range matches {
filtered = append(filtered, color.Highlight(lines[idx], indices[i], 209)) filtered = append(filtered, color.Highlight(ll[idx], indices[i], 209))
} }
return filtered, nil return filtered, nil
} }
func (l *Log) fireLogBuffChanged(lines *dao.LogItems) { func (l *Log) fireLogBuffChanged(index int) {
ll := make([][]byte, lines.Len()) ll := make([][]byte, l.lines.Len()-index)
if l.filter == "" { if l.filter == "" {
lines.Render(l.logOptions.ShowTimestamp, ll) l.lines.Render(index, l.logOptions.ShowTimestamp, ll)
} else { } else {
ff, err := l.applyFilter(l.filter) ff, err := l.applyFilter(index, l.filter)
if err != nil { if err != nil {
l.fireLogError(err) l.fireLogError(err)
return return
} }
ll = ff ll = ff
} }
if len(ll) > 0 { if len(ll) > 0 {
l.fireLogChanged(ll) l.fireLogChanged(ll)
} }
} }
func (l *Log) fireLogResume() {
for _, lis := range l.listeners {
lis.LogResume()
}
}
func (l *Log) fireCanceled() {
for _, lis := range l.listeners {
lis.LogCanceled()
}
}
func (l *Log) fireLogError(err error) { func (l *Log) fireLogError(err error) {
for _, lis := range l.listeners { for _, lis := range l.listeners {
lis.LogFailed(err) lis.LogFailed(err)
@ -371,7 +427,13 @@ func (l *Log) fireLogChanged(lines [][]byte) {
} }
func (l *Log) fireLogCleared() { func (l *Log) fireLogCleared() {
for _, lis := range l.listeners { var ll []LogsListener
l.mx.RLock()
{
ll = l.listeners
}
l.mx.RUnlock()
for _, lis := range ll {
lis.LogCleared() lis.LogCleared()
} }
} }

View File

@ -19,15 +19,14 @@ func TestUpdateLogs(t *testing.T) {
v := newMockLogView() v := newMockLogView()
m.AddListener(v) m.AddListener(v)
c := make(dao.LogChan) c := make(dao.LogChan, 2)
go func() { ctx, cancel := context.WithCancel(context.Background())
m.updateLogs(context.Background(), c) defer cancel()
}() go m.updateLogs(ctx, c)
for i := 0; i < 2*size; i++ { for i := 0; i < 2*size; i++ {
c <- dao.NewLogItemFromString("line" + strconv.Itoa(i)) c <- dao.NewLogItemFromString("line" + strconv.Itoa(i))
} }
close(c)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
assert.Equal(t, size, v.count) assert.Equal(t, size, v.count)
@ -45,11 +44,12 @@ func BenchmarkUpdateLogs(b *testing.B) {
go func() { go func() {
m.updateLogs(context.Background(), c) m.updateLogs(context.Background(), c)
}() }()
item := dao.NewLogItem([]byte("\033[0;38m2018-12-14T10:36:43.326972-07:00 \033[0;32mblee line"))
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
c <- dao.NewLogItemFromString("line" + strconv.Itoa(n)) c <- item
} }
close(c) close(c)
} }
@ -75,5 +75,8 @@ func newMockLogView() *mockLogView {
func (t *mockLogView) LogChanged(ll [][]byte) { func (t *mockLogView) LogChanged(ll [][]byte) {
t.count += len(ll) t.count += len(ll)
} }
func (t *mockLogView) LogStop() {}
func (t *mockLogView) LogCanceled() {}
func (t *mockLogView) LogResume() {}
func (t *mockLogView) LogCleared() {} func (t *mockLogView) LogCleared() {}
func (t *mockLogView) LogFailed(err error) {} func (t *mockLogView) LogFailed(err error) {}

View File

@ -1,6 +1,7 @@
package model_test package model_test
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"testing" "testing"
@ -32,9 +33,8 @@ func TestLogFullBuffer(t *testing.T) {
m.Notify() m.Notify()
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
// assert.Equal(t, data.Items()[4:].Lines(false), v.data)
} }
func TestLogFilter(t *testing.T) { func TestLogFilter(t *testing.T) {
@ -79,13 +79,13 @@ func TestLogFilter(t *testing.T) {
m.Notify() m.Notify()
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, u.e, len(v.data)) assert.Equal(t, u.e, len(v.data))
m.ClearFilter() m.ClearFilter()
assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 3, v.clearCalled) assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, size, len(v.data)) assert.Equal(t, size, len(v.data))
}) })
@ -99,7 +99,10 @@ func TestLogStartStop(t *testing.T) {
v := newTestView() v := newTestView()
m.AddListener(v) m.AddListener(v)
m.Start() c := make(dao.LogChan, 2)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
m.Start(ctx, c)
data := dao.NewLogItems() data := dao.NewLogItems()
data.Add(dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")) data.Add(dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2"))
for _, d := range data.Items() { for _, d := range data.Items() {
@ -109,7 +112,7 @@ func TestLogStartStop(t *testing.T) {
m.Stop() m.Stop()
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.clearCalled)
assert.Equal(t, 1, v.errCalled) assert.Equal(t, 1, v.errCalled)
assert.Equal(t, 2, len(v.data)) assert.Equal(t, 2, len(v.data))
} }
@ -132,7 +135,7 @@ func TestLogClear(t *testing.T) {
m.Clear() m.Clear()
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, 0, len(v.data)) assert.Equal(t, 0, len(v.data))
} }
@ -151,7 +154,9 @@ func TestLogBasic(t *testing.T) {
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, data.Lines(false), v.data) ll := make([][]byte, data.Len())
data.Lines(0, false, ll)
assert.Equal(t, ll, v.data)
} }
func TestLogAppend(t *testing.T) { func TestLogAppend(t *testing.T) {
@ -163,7 +168,9 @@ func TestLogAppend(t *testing.T) {
items := dao.NewLogItems() items := dao.NewLogItems()
items.Add(dao.NewLogItemFromString("blah blah")) items.Add(dao.NewLogItemFromString("blah blah"))
m.Set(items) m.Set(items)
assert.Equal(t, items.Lines(false), v.data) ll := make([][]byte, items.Len())
items.Lines(0, false, ll)
assert.Equal(t, ll, v.data)
data := dao.NewLogItems() data := dao.NewLogItems()
data.Add( data.Add(
@ -174,7 +181,9 @@ func TestLogAppend(t *testing.T) {
m.Append(d) m.Append(d)
} }
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, items.Lines(false), v.data) ll = make([][]byte, items.Len())
items.Lines(0, false, ll)
assert.Equal(t, ll, v.data)
m.Notify() m.Notify()
assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.dataCalled)
@ -203,7 +212,7 @@ func TestLogTimedout(t *testing.T) {
} }
m.Notify() m.Notify()
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m" const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m"
assert.Equal(t, e, string(v.data[0])) assert.Equal(t, e, string(v.data[0]))
@ -215,9 +224,13 @@ func TestToggleAllContainers(t *testing.T) {
m := model.NewLog(client.NewGVR(""), opts, 10*time.Millisecond) m := model.NewLog(client.NewGVR(""), opts, 10*time.Millisecond)
m.Init(makeFactory()) m.Init(makeFactory())
assert.Equal(t, "blee", m.GetContainer()) assert.Equal(t, "blee", m.GetContainer())
m.ToggleAllContainers() ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := make(dao.LogChan, 2)
m.ToggleAllContainers(ctx, c)
assert.Equal(t, "", m.GetContainer()) assert.Equal(t, "", m.GetContainer())
m.ToggleAllContainers() m.ToggleAllContainers(ctx, c)
assert.Equal(t, "blee", m.GetContainer()) assert.Equal(t, "blee", m.GetContainer())
} }
@ -245,16 +258,17 @@ func newTestView() *testView {
return &testView{} return &testView{}
} }
func (t *testView) LogCanceled() {}
func (t *testView) LogStop() {}
func (t *testView) LogResume() {}
func (t *testView) LogChanged(ll [][]byte) { func (t *testView) LogChanged(ll [][]byte) {
t.data = ll t.data = ll
t.dataCalled++ t.dataCalled++
} }
func (t *testView) LogCleared() { func (t *testView) LogCleared() {
t.clearCalled++ t.clearCalled++
t.data = nil t.data = nil
} }
func (t *testView) LogFailed(err error) { func (t *testView) LogFailed(err error) {
fmt.Println("LogErr", err) fmt.Println("LogErr", err)
t.errCalled++ t.errCalled++

View File

@ -164,7 +164,7 @@ func (t *Table) Peek() render.TableData {
} }
func (t *Table) updater(ctx context.Context) { func (t *Table) updater(ctx context.Context) {
defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr) defer log.Debug().Msgf("TABLE-UPDATER canceled -- %q", t.gvr)
bf := backoff.NewExponentialBackOff() bf := backoff.NewExponentialBackOff()
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -139,7 +139,7 @@ func TestTableGenericHydrate(t *testing.T) {
// Helpers... // Helpers...
func mustLoad(n string) *unstructured.Unstructured { func mustLoad(n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -151,7 +151,7 @@ func mustLoad(n string) *unstructured.Unstructured {
} }
func load(t *testing.T, n string) *unstructured.Unstructured { func load(t *testing.T, n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err) assert.Nil(t, err)
var o unstructured.Unstructured var o unstructured.Unstructured
err = json.Unmarshal(raw, &o) err = json.Unmarshal(raw, &o)
@ -160,7 +160,7 @@ func load(t *testing.T, n string) *unstructured.Unstructured {
} }
func raw(t *testing.T, n string) []byte { func raw(t *testing.T, n string) []byte {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err) assert.Nil(t, err)
return raw return raw
} }

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -122,7 +122,7 @@ func makeTableFactory() tableFactory {
} }
func mustLoad(n string) *unstructured.Unstructured { func mustLoad(n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -5,7 +5,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -138,7 +137,7 @@ func (b *Benchmark) save(cluster string, r io.Reader) error {
} }
}() }()
bb, err := ioutil.ReadAll(r) bb, err := io.ReadAll(r)
if err != nil { if err != nil {
return err return err
} }

20
internal/port/ann.go Normal file
View File

@ -0,0 +1,20 @@
package port
import (
"errors"
)
type Annotations map[string]string
func (a Annotations) PreferredPorts(specs ContainerPortSpecs) (PFAnns, error) {
if len(specs) == 0 {
return nil, errors.New("no exposed ports")
}
value, ok := a[K9sPortForwardsKey]
if !ok {
return PFAnns{specs[0].ToPFAnn()}, nil
}
return specs.MatchAnnotations(value), nil
}

93
internal/port/ann_test.go Normal file
View File

@ -0,0 +1,93 @@
package port_test
import (
"errors"
"testing"
"github.com/derailed/k9s/internal/port"
"github.com/stretchr/testify/assert"
)
func TestPreferredPorts(t *testing.T) {
uu := map[string]struct {
anns port.Annotations
specs port.ContainerPortSpecs
err error
e string
}{
"no-ports": {
anns: port.Annotations{
port.K9sPortForwardsKey: "c1::4321:p1",
},
err: errors.New("no exposed ports"),
},
"no-annotations": {
specs: port.ContainerPortSpecs{
{Container: "c1", PortName: "p1", PortNum: "1234"},
},
e: "c1::1234:p1",
},
"single-numb": {
anns: port.Annotations{
port.K9sPortForwardsKey: "c1::4321:1234",
},
specs: port.ContainerPortSpecs{
{Container: "c1", PortName: "p1", PortNum: "1234"},
},
e: "c1::4321:1234/1234",
},
"single-same": {
anns: port.Annotations{
port.K9sPortForwardsKey: "c1::1234",
},
specs: port.ContainerPortSpecs{
{Container: "c1", PortName: "p1", PortNum: "1234"},
},
e: "c1::1234:1234/1234",
},
"single-mismatch": {
anns: port.Annotations{
port.K9sPortForwardsKey: "c2::4321:p1",
},
specs: port.ContainerPortSpecs{
{Container: "c1", PortName: "p1", PortNum: "1234"},
},
},
"multi": {
anns: port.Annotations{
port.K9sPortForwardsKey: "c1::4321:1234,c1::5432:2345",
},
specs: port.ContainerPortSpecs{
{Container: "c1", PortName: "p1", PortNum: "1234"},
{Container: "c1", PortName: "p2", PortNum: "2345"},
},
e: "c1::4321:1234/1234,c1::5432:2345/2345",
},
"multi-mismatch": {
anns: port.Annotations{
port.K9sPortForwardsKey: "c1::4321:1234,c1::5432:2345",
},
specs: port.ContainerPortSpecs{
{Container: "c1", PortName: "p1", PortNum: "1234"},
{Container: "c2", PortName: "p3", PortNum: "2345"},
},
e: "c1::4321:1234/1234",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
anns, err := u.anns.PreferredPorts(u.specs)
assert.Equal(t, u.err, err)
if err != nil {
return
}
pfs, err := port.ParsePFs(u.e)
if err != nil {
pfs = port.PFAnns{}
}
assert.Equal(t, pfs, anns)
})
}
}

View File

@ -0,0 +1,147 @@
package port
import (
"strconv"
"strings"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
// ContainerPortSpecs represents a container exposed ports.
type ContainerPortSpecs []ContainerPortSpec
func (c ContainerPortSpecs) Dump() string {
ss := make([]string, 0, len(c))
for _, spec := range c {
ss = append(ss, spec.String())
}
return strings.Join(ss, "\n")
}
// ToTunnels convert port specs to tunnels.
func (c ContainerPortSpecs) ToTunnels(address string) PortTunnels {
tt := make(PortTunnels, 0, len(c))
for _, spec := range c {
tt = append(tt, spec.ToTunnel(address))
}
return tt
}
// Find finds a matching container port.
func (c ContainerPortSpecs) Find(pf *PFAnn) (ContainerPortSpec, bool) {
for _, spec := range c {
if spec.Match(pf) {
return spec, true
}
}
return ContainerPortSpec{}, false
}
// Match checks if container ports match a pf annotation.
func (c ContainerPortSpecs) Match(pf *PFAnn) bool {
for _, spec := range c {
if spec.Match(pf) {
return true
}
}
return false
}
func (c ContainerPortSpecs) MatchAnnotations(s string) PFAnns {
pfs, err := ParsePFs(s)
if err != nil {
return nil
}
mm := make(PFAnns, 0, len(c))
for _, pf := range pfs {
if pf.Match(c) {
mm = append(mm, pf)
}
}
return mm
}
// FromContainerPorts hydrates from a pod container specification.
func FromContainerPorts(co string, pp []v1.ContainerPort) ContainerPortSpecs {
specs := make(ContainerPortSpecs, 0, len(pp))
for _, p := range pp {
if p.Protocol != v1.ProtocolTCP {
continue
}
specs = append(specs, NewPortSpec(co, p.Name, p.ContainerPort))
}
return specs
}
// ContainerPortSpec represents a container port specification.
type ContainerPortSpec struct {
Container string
PortName string
PortNum string
}
// NewPortSpec returns a new instance.
func NewPortSpec(co, portName string, port int32) ContainerPortSpec {
return ContainerPortSpec{
Container: co,
PortName: portName,
PortNum: strconv.Itoa(int(port)),
}
}
func (c ContainerPortSpec) ToTunnel(address string) PortTunnel {
return PortTunnel{
Address: address,
LocalPort: c.PortNum,
ContainerPort: c.PortNum,
}
}
func (c ContainerPortSpec) Port() intstr.IntOrString {
if c.PortName != "" {
return intstr.Parse(c.PortName)
}
return intstr.Parse(c.PortNum)
}
func (c ContainerPortSpec) ToPFAnn() *PFAnn {
return &PFAnn{
Container: c.Container,
ContainerPort: c.Port(),
LocalPort: c.PortNum,
}
}
// Match checks if the container spec matches an annotation.
func (c ContainerPortSpec) Match(ann *PFAnn) bool {
if c.Container != ann.Container {
return false
}
switch ann.ContainerPort.Type {
case intstr.String:
return c.PortName == ann.ContainerPort.String()
case intstr.Int:
return c.PortNum == ann.ContainerPort.String()
default:
return false
}
}
// String dumps spec to string.
func (c ContainerPortSpec) String() string {
s := c.Container + "::" + c.PortNum
if c.PortName != "" {
s += "(" + c.PortName + ")"
}
return s
}

View File

@ -0,0 +1,138 @@
package port_test
import (
"testing"
"github.com/derailed/k9s/internal/port"
"github.com/stretchr/testify/assert"
)
func TestContainerPortSpecMatch(t *testing.T) {
uu := map[string]struct {
ann string
spec port.ContainerPortSpec
e bool
}{
"full": {
ann: "c1::4321:1234",
spec: port.ContainerPortSpec{
Container: "c1",
PortNum: "1234",
},
e: true,
},
"no-port-name": {
ann: "c1::4321:p1/1234",
spec: port.ContainerPortSpec{
Container: "c1",
PortName: "p1",
PortNum: "1234",
},
e: true,
},
"port-name-hosed": {
ann: "c1::4321:blee/1234",
spec: port.ContainerPortSpec{
Container: "c1",
PortName: "fred",
PortNum: "1234",
},
},
"container-name-hosed": {
ann: "c2::4321:fred/1234",
spec: port.ContainerPortSpec{
Container: "c1",
PortName: "blee",
PortNum: "1234",
},
},
"port-num-hosed": {
ann: "c2::4321:1235",
spec: port.ContainerPortSpec{
Container: "c1",
PortNum: "1234",
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pf, err := port.ParsePF(u.ann)
assert.Nil(t, err)
assert.Equal(t, u.e, u.spec.Match(pf))
})
}
}
func TestContainerPortSpecString(t *testing.T) {
uu := map[string]struct {
spec port.ContainerPortSpec
e string
}{
"full": {
spec: port.NewPortSpec("c1", "p1", 1234),
e: "c1::1234(p1)",
},
"no-name": {
spec: port.NewPortSpec("c1", "", 1234),
e: "c1::1234",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.spec.String())
})
}
}
func TestContainerPortSpecsMatch(t *testing.T) {
uu := map[string]struct {
ann string
specs port.ContainerPortSpecs
e bool
}{
"full": {
ann: "c1::4321:p1",
specs: port.ContainerPortSpecs{
port.NewPortSpec("c1", "p1", 1234),
port.NewPortSpec("c2", "p2", 1235),
},
e: true,
},
"no-name": {
ann: "c1::4321",
specs: port.ContainerPortSpecs{
port.NewPortSpec("c1", "", 4321),
port.NewPortSpec("c2", "p2", 1235),
},
e: true,
},
"name-hosed": {
ann: "c1::4321:p4",
specs: port.ContainerPortSpecs{
port.NewPortSpec("c1", "p1", 1234),
port.NewPortSpec("c2", "p2", 1235),
},
},
"numb-hosed": {
ann: "c1::4321:1235",
specs: port.ContainerPortSpecs{
port.NewPortSpec("c1", "p1", 1234),
port.NewPortSpec("c2", "p2", 1236),
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pf, err := port.ParsePF(u.ann)
assert.Nil(t, err)
assert.Equal(t, u.e, u.specs.Match(pf))
})
}
}

102
internal/port/pf.go Normal file
View File

@ -0,0 +1,102 @@
package port
import (
"errors"
"fmt"
"regexp"
"strings"
"k8s.io/apimachinery/pkg/util/intstr"
)
const (
// K9sAutoPortForwardKey represents an auto portforwards annotation.
K9sAutoPortForwardsKey = "k9scli.io/auto-portforwards"
// K9sPortForwardKey represents a portforwards annotation.
K9sPortForwardsKey = "k9scli.io/portforwards"
)
var pfRX = regexp.MustCompile(`\A([\w-]+)::(\d*):?(\d*|[\w-]*)/?(\d+)?\z`)
// PFAnn represents a portforward annotation value.
// Shape: container/portname|portNum:localPort
type PFAnn struct {
Container string
ContainerPort intstr.IntOrString
LocalPort string
containerPortNum string
}
// ParsePF hydrate a portforward annotation from string.
func ParsePF(ann string) (*PFAnn, error) {
var pf PFAnn
r := pfRX.FindStringSubmatch(strings.TrimSpace(ann))
if len(r) < 4 {
return &pf, fmt.Errorf("invalid pf annotation %s", ann)
}
pf.Container = r[1]
pf.LocalPort, pf.ContainerPort = r[2], intstr.Parse(r[3])
if r[3] == "" {
pf.ContainerPort = intstr.Parse(pf.LocalPort)
}
// Testing only!
if len(r) == 5 && r[4] != "" {
pf.containerPortNum = r[4]
}
if pf.LocalPort == "" {
pf.LocalPort = pf.containerPortNum
}
return &pf, nil
}
// Match checks if annotation matches any of the container ports.
func (p *PFAnn) Match(ss ContainerPortSpecs) bool {
for _, s := range ss {
if s.Match(p) {
p.containerPortNum = s.PortNum
return true
}
}
return false
}
func (p *PFAnn) AsSpec() string {
s := p.Container + "::"
if p.containerPortNum != "" {
return s + p.containerPortNum
}
return s + p.LocalPort
}
// String dumps the annotation.
func (p *PFAnn) String() string {
return p.Container + "::" + p.LocalPort + ":" + p.containerPortNum
}
func (p *PFAnn) PortNum() (string, error) {
if p.ContainerPort.Type == intstr.Int {
return p.ContainerPort.String(), nil
}
if p.containerPortNum != "" {
return p.containerPortNum, nil
}
return "", errors.New("no port number assigned")
}
func (p *PFAnn) ToTunnel(address string) (PortTunnel, error) {
var pt PortTunnel
port, err := p.PortNum()
if err != nil {
return pt, err
}
pt.Address, pt.Container = address, p.Container
pt.ContainerPort, pt.LocalPort = port, p.LocalPort
return pt, nil
}

207
internal/port/pf_test.go Normal file
View File

@ -0,0 +1,207 @@
package port_test
import (
"errors"
"testing"
"github.com/derailed/k9s/internal/port"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/intstr"
)
func TestParsePF(t *testing.T) {
uu := map[string]struct {
exp string
container string
containerPort intstr.IntOrString
localPort string
e error
}{
"full-numbs": {
exp: "c1::4321:1234",
container: "c1",
containerPort: intstr.Parse("1234"),
localPort: "4321",
},
"full-named": {
exp: "c1::4321:p1/1234",
container: "c1",
containerPort: intstr.Parse("p1"),
localPort: "4321",
},
"just-named": {
exp: "c1::p1/1234",
container: "c1",
containerPort: intstr.Parse("p1"),
localPort: "1234",
},
"just-num": {
exp: "c1::1234",
container: "c1",
containerPort: intstr.Parse("1234"),
localPort: "1234",
},
"toast": {
exp: "c1:4321:1234",
e: errors.New("invalid pf annotation c1:4321:1234"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pf, err := port.ParsePF(u.exp)
assert.Equal(t, u.e, err)
if err != nil {
return
}
assert.Equal(t, u.container, pf.Container)
assert.Equal(t, u.containerPort, pf.ContainerPort)
assert.Equal(t, u.localPort, pf.LocalPort)
})
}
}
func TestPFMatch(t *testing.T) {
uu := map[string]struct {
exp string
specs port.ContainerPortSpecs
err error
e bool
}{
"match": {
exp: "c1::1234",
specs: port.ContainerPortSpecs{
{Container: "c1", PortNum: "1234"},
},
e: true,
},
"match-portnum": {
exp: "c1::4321:1234",
specs: port.ContainerPortSpecs{
{Container: "c1", PortNum: "1234"},
},
e: true,
},
"no-match": {
exp: "c1::1235",
specs: port.ContainerPortSpecs{
{Container: "c1", PortNum: "1234"},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pf, err := port.ParsePF(u.exp)
assert.Equal(t, u.err, err)
if err != nil {
return
}
assert.Equal(t, u.e, pf.Match(u.specs))
})
}
}
func TestPFPortNum(t *testing.T) {
uu := map[string]struct {
exp string
err error
e string
}{
"port-name": {
exp: "c1::4321:1234",
e: "1234",
},
"port-number": {
exp: "c1::4321:1234",
e: "1234",
},
"missing-port-number": {
exp: "c1::p1",
err: errors.New("no port number assigned"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pf, err := port.ParsePF(u.exp)
assert.Nil(t, err)
n, err := pf.PortNum()
assert.Equal(t, u.err, err)
if err != nil {
return
}
assert.Equal(t, u.e, n)
})
}
}
func TestPFToTunnel(t *testing.T) {
uu := map[string]struct {
exp string
err error
e port.PortTunnel
}{
"port-name": {
exp: "c1::p1/1234",
e: port.PortTunnel{
Address: "blee",
Container: "c1",
LocalPort: "1234",
ContainerPort: "1234",
},
},
"port-numb": {
exp: "c1::4321:1234",
e: port.PortTunnel{
Address: "blee",
Container: "c1",
LocalPort: "4321",
ContainerPort: "1234",
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pf, err := port.ParsePF(u.exp)
assert.Nil(t, err)
pt, err := pf.ToTunnel("blee")
assert.Equal(t, u.err, err)
if err != nil {
return
}
assert.Equal(t, u.e, pt)
})
}
}
func TestPFString(t *testing.T) {
uu := map[string]struct {
exp string
err error
e string
}{
"port-name": {
exp: "c1::p1/1234",
e: "c1::1234:1234",
},
"port-numb": {
exp: "c1::4321:1234/1234",
e: "c1::4321:1234",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pf, err := port.ParsePF(u.exp)
assert.Nil(t, err)
assert.Equal(t, u.e, pf.String())
})
}
}

92
internal/port/pfs.go Normal file
View File

@ -0,0 +1,92 @@
package port
import (
"fmt"
"strings"
)
// PortCheck checks if port is free on host.
type PortChecker func(PortTunnel) bool
// PFAnns represents a collection of port forward annotations.
type PFAnns []*PFAnn
// ToPortSpec returns a container port and local port definitions.
func (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (string, string) {
specs, lps := make([]string, 0, len(aa)), make([]string, 0, len(aa))
for _, a := range aa {
specs = append(specs, a.AsSpec())
if a.LocalPort == "" {
if spec, ok := pp.Find(a); ok {
a.LocalPort = spec.PortNum
}
}
if a.LocalPort != "" {
lps = append(lps, a.LocalPort)
}
}
return strings.Join(specs, ","), strings.Join(lps, ",")
}
func (aa PFAnns) ToTunnels(address string, pp ContainerPortSpecs, available PortChecker) (PortTunnels, error) {
pts := make(PortTunnels, 0, len(aa))
for _, a := range aa {
if !a.Match(pp) {
return nil, fmt.Errorf("ann does not match container port specs")
}
pt, err := a.ToTunnel(address)
if err != nil {
return pts, err
}
if !available(pt) {
return pts, fmt.Errorf("Port %s is not available on host", pt.LocalPort)
}
pts = append(pts, pt)
}
return pts, nil
}
// ParsePFs hydrates a collection of portforward annotations.
func ParsePFs(ann string) (PFAnns, error) {
ss := strings.Split(ann, ",")
pp := make(PFAnns, 0, len(ss))
for _, s := range ss {
f, err := ParsePF(s)
if err != nil {
return nil, err
}
pp = append(pp, f)
}
return pp, nil
}
func ToTunnels(address, specs, localPorts string) (PortTunnels, error) {
pp, lps := strings.Split(specs, ","), strings.Split(localPorts, ",")
if len(pp) != len(lps) {
return nil, fmt.Errorf("spec to local port count mismatch. Expected %d but got %d", len(pp), len(lps))
}
pts := make(PortTunnels, 0, len(pp))
for i, p := range pp {
a, err := ParsePF(p)
if err != nil {
return nil, err
}
n, err := a.PortNum()
if err != nil {
return nil, err
}
pts = append(pts, PortTunnel{
Address: address,
Container: a.Container,
ContainerPort: n,
LocalPort: lps[i],
})
}
return pts, nil
}

185
internal/port/pfs_test.go Normal file
View File

@ -0,0 +1,185 @@
package port_test
import (
"errors"
"testing"
"github.com/derailed/k9s/internal/port"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/intstr"
)
func TestParsePFs(t *testing.T) {
uu := map[string]struct {
exp string
pfs port.PFAnns
e error
}{
"single": {
exp: "c2::4321:1234",
pfs: port.PFAnns{
{Container: "c2", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"},
},
},
"multi": {
exp: "c1::4321:1234,c2::6666:6543",
pfs: port.PFAnns{
{Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"},
{Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"},
},
},
"spaces": {
exp: " c1::4321:1234 , c2::6666:6543 ",
pfs: port.PFAnns{
{Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"},
{Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"},
},
},
"toast": {
exp: "c1::p1:1234,c2::4321",
e: errors.New("invalid pf annotation c1::p1:1234"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pfs, err := port.ParsePFs(u.exp)
assert.Equal(t, u.e, err)
if err != nil {
return
}
assert.Equal(t, u.pfs, pfs)
})
}
}
func TestPFsToTunnel(t *testing.T) {
uu := map[string]struct {
exp string
specs port.ContainerPortSpecs
pts port.PortTunnels
e error
}{
"single": {
exp: "c2::4321:1234",
specs: port.ContainerPortSpecs{
{Container: "c2", PortName: "p1", PortNum: "1234"},
},
pts: port.PortTunnels{
{Address: "fred", Container: "c2", ContainerPort: "1234", LocalPort: "4321"},
},
},
"hosed": {
exp: "c2::p2",
specs: port.ContainerPortSpecs{
{Container: "c2", PortName: "p1", PortNum: "1234"},
},
pts: port.PortTunnels{
{Address: "fred", Container: "c2", ContainerPort: "1234", LocalPort: "4321"},
},
e: errors.New("ann does not match container port specs"),
},
}
f := func(port.PortTunnel) bool {
return true
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pfs, err := port.ParsePFs(u.exp)
assert.Nil(t, err)
pts, err := pfs.ToTunnels("fred", u.specs, f)
assert.Equal(t, u.e, err)
if err != nil {
return
}
assert.Equal(t, u.pts, pts)
})
}
}
func TestPFsToPortSpec(t *testing.T) {
uu := map[string]struct {
exp string
spec, port string
specs port.ContainerPortSpecs
e error
}{
"single": {
exp: "c2::4321:p2/1234",
spec: "c2::1234",
port: "4321",
specs: port.ContainerPortSpecs{
{Container: "c2", PortNum: "1234"},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pfs, err := port.ParsePFs(u.exp)
assert.Equal(t, u.e, err)
if err != nil {
return
}
spec, port := pfs.ToPortSpec(u.specs)
assert.Equal(t, u.spec, spec)
assert.Equal(t, u.port, port)
})
}
}
func TestToTunnels(t *testing.T) {
uu := map[string]struct {
specs, ports string
tunnels port.PortTunnels
err error
}{
"single": {
specs: "c2::4321:p2/1234",
ports: "4321",
tunnels: port.PortTunnels{
{
Address: "blee",
LocalPort: "4321",
Container: "c2",
ContainerPort: "1234",
},
},
},
"multi": {
specs: "c1::5432:2345/2345,c2::4321:p2/1234",
ports: "5432,4321",
tunnels: port.PortTunnels{
{
Address: "blee",
LocalPort: "5432",
Container: "c1",
ContainerPort: "2345",
},
{
Address: "blee",
LocalPort: "4321",
Container: "c2",
ContainerPort: "1234",
},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
tt, err := port.ToTunnels("blee", u.specs, u.ports)
assert.Equal(t, u.err, err)
if err != nil {
return
}
assert.Equal(t, u.tunnels, tt)
})
}
}

49
internal/port/tunnel.go Normal file
View File

@ -0,0 +1,49 @@
package port
import (
"fmt"
"net"
)
// PortTunnels represents a collection of tunnels.
type PortTunnels []PortTunnel
func (t PortTunnels) CheckAvailable() error {
for _, pt := range t {
if !IsPortFree(pt) {
return fmt.Errorf("port %s is not available on host", pt.LocalPort)
}
}
return nil
}
// PortTunnel represents a host tunnel port mapper.
type PortTunnel struct {
Address, Container, LocalPort, ContainerPort string
}
func NewPortTunnel(a, co, lp, cp string) PortTunnel {
return PortTunnel{
Address: a,
Container: co,
LocalPort: lp,
ContainerPort: cp,
}
}
// PortMap returns a port mapping.
func (t PortTunnel) PortMap() string {
if t.LocalPort == "" {
t.LocalPort = t.ContainerPort
}
return t.LocalPort + ":" + t.ContainerPort
}
func IsPortFree(t PortTunnel) bool {
s, err := net.Listen("tcp", fmt.Sprintf("%s:%s", t.Address, t.LocalPort))
if err != nil {
return false
}
return s.Close() == nil
}

View File

@ -0,0 +1,32 @@
package port_test
import (
"testing"
"github.com/derailed/k9s/internal/port"
"github.com/stretchr/testify/assert"
)
func TestPortTunnelMap(t *testing.T) {
uu := map[string]struct {
pt port.PortTunnel
coPort, locPort string
e string
}{
"plain": {
pt: port.PortTunnel{
Address: "localhost",
LocalPort: "1234",
ContainerPort: "4321",
},
e: "1234:4321",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.pt.PortMap())
})
}
}

View File

@ -3,7 +3,6 @@ package render
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@ -97,7 +96,7 @@ func (Benchmark) diagnose(ns string, ff Fields) error {
// Helpers... // Helpers...
func (Benchmark) readFile(file string) (string, error) { func (Benchmark) readFile(file string) (string, error) {
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,7 +1,7 @@
package render package render
import ( import (
"io/ioutil" "os"
"testing" "testing"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -38,7 +38,7 @@ func TestAugmentRow(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
data, err := ioutil.ReadFile(u.file) data, err := os.ReadFile(u.file)
assert.Nil(t, err) assert.Nil(t, err)
fields := make(Fields, 8) fields := make(Fields, 8)

View File

@ -35,10 +35,10 @@ func (Dir) Render(o interface{}, ns string, r *Row) error {
} }
name := "🦄 " name := "🦄 "
if d.Info.IsDir() { if d.Entry.IsDir() {
name = "📁 " name = "📁 "
} }
name += d.Info.Name() name += d.Entry.Name()
r.ID, r.Fields = d.Path, append(r.Fields, name) r.ID, r.Fields = d.Path, append(r.Fields, name)
return nil return nil
@ -49,8 +49,8 @@ func (Dir) Render(o interface{}, ns string, r *Row) error {
// DirRes represents an alias resource. // DirRes represents an alias resource.
type DirRes struct { type DirRes struct {
Info os.FileInfo Entry os.DirEntry
Path string Path string
} }
// GetObjectKind returns a schema object. // GetObjectKind returns a schema object.

View File

@ -26,7 +26,7 @@ func TestPortForwardRender(t *testing.T) {
"blee", "blee",
"fred", "fred",
"co", "co",
"p1", "p1:p2",
"http://0.0.0.0:p1/", "http://0.0.0.0:p1/",
"1", "1",
"1", "1",
@ -47,8 +47,8 @@ func (f fwd) Container() string {
return "co" return "co"
} }
func (f fwd) Ports() []string { func (f fwd) Port() string {
return []string{"p1"} return "p1:p2"
} }
func (f fwd) Active() bool { func (f fwd) Active() bool {

View File

@ -19,7 +19,7 @@ type Forwarder interface {
Container() string Container() string
// Ports returns container exposed ports. // Ports returns container exposed ports.
Ports() []string Port() string
// Active returns forwarder current state. // Active returns forwarder current state.
Active() bool Active() bool
@ -60,7 +60,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
return fmt.Errorf("expecting a ForwardRes but got %T", o) return fmt.Errorf("expecting a ForwardRes but got %T", o)
} }
ports := strings.Split(pf.Ports()[0], ":") ports := strings.Split(pf.Port(), ":")
ns, n := client.Namespaced(pf.Path()) ns, n := client.Namespaced(pf.Path())
r.ID = pf.Path() r.ID = pf.Path()
@ -68,7 +68,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
ns, ns,
trimContainer(n), trimContainer(n),
pf.Container(), pf.Container(),
strings.Join(pf.Ports(), ","), pf.Port(),
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]), UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
AsThousands(int64(pf.Config.C)), AsThousands(int64(pf.Config.C)),
AsThousands(int64(pf.Config.N)), AsThousands(int64(pf.Config.N)),
@ -82,11 +82,13 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
// Helpers... // Helpers...
func trimContainer(n string) string { func trimContainer(n string) string {
tokens := strings.Split(n, ":") tokens := strings.Split(n, "|")
if len(tokens) == 0 { if len(tokens) == 0 {
return n return n
} }
return tokens[0] _, name := client.Namespaced(tokens[0])
return name
} }
// UrlFor computes fq url for a given benchmark configuration. // UrlFor computes fq url for a given benchmark configuration.

View File

@ -3,7 +3,7 @@ package render_test
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -13,7 +13,7 @@ import (
// Helpers... // Helpers...
func load(t testing.TB, n string) *unstructured.Unstructured { func load(t testing.TB, n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err) assert.Nil(t, err)
var o unstructured.Unstructured var o unstructured.Unstructured

View File

@ -41,7 +41,7 @@ func NewApp(cfg *config.Config, context string) *App {
a.views = map[string]tview.Primitive{ a.views = map[string]tview.Primitive{
"menu": NewMenu(a.Styles), "menu": NewMenu(a.Styles),
"logo": NewLogo(a.Styles), "logo": NewLogo(a.Styles),
"prompt": NewPrompt(a.Config.K9s.NoIcons, a.Styles), "prompt": NewPrompt(&a, a.Config.K9s.NoIcons, a.Styles),
"crumbs": NewCrumbs(a.Styles), "crumbs": NewCrumbs(a.Styles),
} }
@ -60,6 +60,9 @@ func (a *App) Init() {
// QueueUpdate queues up a ui action. // QueueUpdate queues up a ui action.
func (a *App) QueueUpdate(f func()) { func (a *App) QueueUpdate(f func()) {
if a.Application == nil {
return
}
go func() { go func() {
a.Application.QueueUpdate(f) a.Application.QueueUpdate(f)
}() }()
@ -67,6 +70,9 @@ func (a *App) QueueUpdate(f func()) {
// QueueUpdateDraw queues up a ui action and redraw the ui. // QueueUpdateDraw queues up a ui action and redraw the ui.
func (a *App) QueueUpdateDraw(f func()) { func (a *App) QueueUpdateDraw(f func()) {
if a.Application == nil {
return
}
go func() { go func() {
a.Application.QueueUpdateDraw(f) a.Application.QueueUpdateDraw(f)
}() }()

View File

@ -50,7 +50,7 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e
log.Warn().Err(err).Msg("CustomView watcher failed") log.Warn().Err(err).Msg("CustomView watcher failed")
return return
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msgf("CustomViewWatcher Done `%s!!", config.K9sViewConfigFile) log.Debug().Msgf("CustomViewWatcher CANCELED `%s!!", config.K9sViewConfigFile)
if err := w.Close(); err != nil { if err := w.Close(); err != nil {
log.Error().Err(err).Msg("Closing CustomView watcher") log.Error().Err(err).Msg("Closing CustomView watcher")
} }
@ -102,7 +102,7 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error
log.Info().Err(err).Msg("Skin watcher failed") log.Info().Err(err).Msg("Skin watcher failed")
return return
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile) log.Debug().Msgf("SkinWatcher CANCELED `%s!!", c.skinFile)
if err := w.Close(); err != nil { if err := w.Close(); err != nil {
log.Error().Err(err).Msg("Closing Skin watcher") log.Error().Err(err).Msg("Closing Skin watcher")
} }

View File

@ -23,10 +23,10 @@ func ShowDelete(styles config.Dialog, pages *ui.Pages, msg string, ok okFunc, ca
SetButtonTextColor(styles.ButtonFgColor.Color()). SetButtonTextColor(styles.ButtonFgColor.Color()).
SetLabelColor(styles.LabelFgColor.Color()). SetLabelColor(styles.LabelFgColor.Color()).
SetFieldTextColor(styles.FieldFgColor.Color()) SetFieldTextColor(styles.FieldFgColor.Color())
f.AddCheckbox("Cascade:", cascade, func(checked bool) { f.AddCheckbox("Cascade:", cascade, func(_ string, checked bool) {
cascade = checked cascade = checked
}) })
f.AddCheckbox("Force:", force, func(checked bool) { f.AddCheckbox("Force:", force, func(_ string, checked bool) {
force = checked force = checked
}) })
f.AddButton("Cancel", func() { f.AddButton("Cancel", func() {

View File

@ -52,7 +52,7 @@ func (f *Flash) StylesChanged(s *config.Styles) {
// Watch watches for flash changes. // Watch watches for flash changes.
func (f *Flash) Watch(ctx context.Context, c model.FlashChan) { func (f *Flash) Watch(ctx context.Context, c model.FlashChan) {
defer log.Debug().Msgf("Flash Canceled!") defer log.Debug().Msgf("Flash Watch Canceled!")
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():

View File

@ -71,6 +71,7 @@ type PromptModel interface {
type Prompt struct { type Prompt struct {
*tview.TextView *tview.TextView
app *App
noIcons bool noIcons bool
icon rune icon rune
styles *config.Styles styles *config.Styles
@ -79,8 +80,9 @@ type Prompt struct {
} }
// NewPrompt returns a new command view. // NewPrompt returns a new command view.
func NewPrompt(noIcons bool, styles *config.Styles) *Prompt { func NewPrompt(app *App, noIcons bool, styles *config.Styles) *Prompt {
p := Prompt{ p := Prompt{
app: app,
styles: styles, styles: styles,
noIcons: noIcons, noIcons: noIcons,
TextView: tview.NewTextView(), TextView: tview.NewTextView(),
@ -183,8 +185,15 @@ func (p *Prompt) activate() {
} }
func (p *Prompt) update(s string) { func (p *Prompt) update(s string) {
p.Clear() f := func() {
p.write(s, "") p.Clear()
p.write(s, "")
}
if p.app == nil {
f()
return
}
p.app.QueueUpdate(f)
} }
func (p *Prompt) suggest(text, suggestion string) { func (p *Prompt) suggest(text, suggestion string) {

View File

@ -10,7 +10,7 @@ import (
) )
func TestCmdNew(t *testing.T) { func TestCmdNew(t *testing.T) {
v := ui.NewPrompt(true, config.NewStyles()) v := ui.NewPrompt(nil, true, config.NewStyles())
model := model.NewFishBuff(':', model.CommandBuffer) model := model.NewFishBuff(':', model.CommandBuffer)
v.SetModel(model) v.SetModel(model)
model.AddListener(v) model.AddListener(v)
@ -23,7 +23,7 @@ func TestCmdNew(t *testing.T) {
func TestCmdUpdate(t *testing.T) { func TestCmdUpdate(t *testing.T) {
model := model.NewFishBuff(':', model.CommandBuffer) model := model.NewFishBuff(':', model.CommandBuffer)
v := ui.NewPrompt(true, config.NewStyles()) v := ui.NewPrompt(nil, true, config.NewStyles())
v.SetModel(model) v.SetModel(model)
model.AddListener(v) model.AddListener(v)
@ -36,7 +36,7 @@ func TestCmdUpdate(t *testing.T) {
func TestCmdMode(t *testing.T) { func TestCmdMode(t *testing.T) {
model := model.NewFishBuff(':', model.CommandBuffer) model := model.NewFishBuff(':', model.CommandBuffer)
v := ui.NewPrompt(true, config.NewStyles()) v := ui.NewPrompt(&ui.App{}, true, config.NewStyles())
v.SetModel(model) v.SetModel(model)
model.AddListener(v) model.AddListener(v)

View File

@ -136,6 +136,7 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler {
clear: true, clear: true,
binary: p.Command, binary: p.Command,
background: p.Background, background: p.Background,
pipes: p.Pipes,
args: args, args: args,
} }
if run(r.App(), opts) { if run(r.App(), opts) {

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"runtime"
"sort" "sort"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -107,8 +108,10 @@ func (a *App) Init(version string, rate int) error {
a.clusterModel = model.NewClusterInfo(a.factory, a.version) a.clusterModel = model.NewClusterInfo(a.factory, a.version)
a.clusterModel.AddListener(a.clusterInfo()) a.clusterModel.AddListener(a.clusterInfo())
a.clusterModel.AddListener(a.statusIndicator()) a.clusterModel.AddListener(a.statusIndicator())
a.clusterModel.Refresh() if a.Conn().ConnectionOK() {
a.clusterInfo().Init() a.clusterModel.Refresh()
a.clusterInfo().Init()
}
a.command = NewCommand(a) a.command = NewCommand(a)
if err := a.command.Init(); err != nil { if err := a.command.Init(); err != nil {
@ -185,6 +188,7 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
func (a *App) bindKeys() { func (a *App) bindKeys() {
a.AddActions(ui.KeyActions{ a.AddActions(ui.KeyActions{
ui.KeyShiftG: ui.NewSharedKeyAction("DumpGOR", a.dumpGOR, false),
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false), tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false), tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false),
ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false), ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false),
@ -193,6 +197,13 @@ func (a *App) bindKeys() {
}) })
} }
func (a *App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey {
bb := make([]byte, 5_000_000)
runtime.Stack(bb, true)
log.Debug().Msgf("GOR\n%s", string(bb))
return evt
}
// ActiveView returns the currently active view. // ActiveView returns the currently active view.
func (a *App) ActiveView() model.Component { func (a *App) ActiveView() model.Component {
return a.Content.GetPrimitive("main").(model.Component) return a.Content.GetPrimitive("main").(model.Component)

View File

@ -12,5 +12,5 @@ func TestAppNew(t *testing.T) {
a := view.NewApp(config.NewConfig(ks{})) a := view.NewApp(config.NewConfig(ks{}))
a.Init("blee", 10) a.Init("blee", 10)
assert.Equal(t, 10, len(a.GetActions())) assert.Equal(t, 11, len(a.GetActions()))
} }

View File

@ -2,7 +2,7 @@ package view
import ( import (
"context" "context"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -71,7 +71,7 @@ func benchDir(cfg *config.Config) string {
} }
func readBenchFile(cfg *config.Config, n string) (string, error) { func readBenchFile(cfg *config.Config, n string) (string, error) {
data, err := ioutil.ReadFile(filepath.Join(benchDir(cfg), n)) data, err := os.ReadFile(filepath.Join(benchDir(cfg), n))
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -7,6 +7,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -31,6 +32,7 @@ type Browser struct {
accessor dao.Accessor accessor dao.Accessor
contextFn ContextFunc contextFn ContextFunc
cancelFn context.CancelFunc cancelFn context.CancelFunc
mx sync.RWMutex
} }
// NewBrowser returns a new browser. // NewBrowser returns a new browser.
@ -140,10 +142,14 @@ func (b *Browser) Start() {
// Stop terminates browser updates. // Stop terminates browser updates.
func (b *Browser) Stop() { func (b *Browser) Stop() {
if b.cancelFn != nil { b.mx.Lock()
b.cancelFn() {
b.cancelFn = nil if b.cancelFn != nil {
b.cancelFn()
b.cancelFn = nil
}
} }
b.mx.Unlock()
b.GetModel().RemoveListener(b) b.GetModel().RemoveListener(b)
b.CmdBuff().RemoveListener(b) b.CmdBuff().RemoveListener(b)
b.Table.Stop() b.Table.Stop()
@ -213,7 +219,12 @@ func (b *Browser) Aliases() []string {
// TableDataChanged notifies view new data is available. // TableDataChanged notifies view new data is available.
func (b *Browser) TableDataChanged(data render.TableData) { func (b *Browser) TableDataChanged(data render.TableData) {
if !b.app.ConOK() || b.cancelFn == nil || !b.app.IsRunning() { var cancel context.CancelFunc
b.mx.RLock()
cancel = b.cancelFn
b.mx.RUnlock()
if !b.app.ConOK() || cancel == nil || !b.app.IsRunning() {
return return
} }

View File

@ -2,6 +2,7 @@ package view
import ( import (
"fmt" "fmt"
"runtime"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
@ -102,7 +103,7 @@ func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
c.app.QueueUpdateDraw(func() { c.app.QueueUpdateDraw(func() {
c.Clear() c.Clear()
c.layout() c.layout()
row := c.setCell(0, curr.Context) row := c.setCell(0, fmt.Sprintf("%s [%d]", curr.Context, runtime.NumGoroutine()))
row = c.setCell(row, curr.Cluster) row = c.setCell(row, curr.Cluster)
row = c.setCell(row, curr.User) row = c.setCell(row, curr.User)
if curr.K9sLatest != "" { if curr.K9sLatest != "" {

View File

@ -4,11 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/port"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -178,89 +178,65 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
ports, ok := c.isForwardable(path) ports, ann, ok := c.listForwardable(path)
if !ok { if !ok {
return nil return nil
} }
ShowPortForwards(c, c.GetTable().Path, ports, "", startFwdCB) ShowPortForwards(c, c.GetTable().Path, ports, ann, startFwdCB)
return nil return nil
} }
func (c *Container) isForwardable(path string) ([]string, bool) { func checkRunningStatus(co string, ss []v1.ContainerStatus) error {
po, err := fetchPod(c.App().factory, c.GetTable().Path)
if err != nil {
return nil, false
}
var co *v1.Container
cc := po.Spec.Containers
for i := range cc {
if cc[i].Name == path {
co = &cc[i]
}
}
if co == nil {
log.Error().Err(fmt.Errorf("unable to locate container named %q", path))
return nil, false
}
var cs *v1.ContainerStatus var cs *v1.ContainerStatus
ss := po.Status.ContainerStatuses
for i := range ss { for i := range ss {
if ss[i].Name == path { if ss[i].Name == co {
cs = &ss[i] cs = &ss[i]
break
} }
} }
if cs == nil { if cs == nil {
log.Error().Err(fmt.Errorf("unable to locate container status for %q", path)) return fmt.Errorf("unable to locate container status for %q", co)
return nil, false
} }
if render.ToContainerState(cs.State) != "Running" { if render.ToContainerState(cs.State) != "Running" {
c.App().Flash().Err(fmt.Errorf("Container %s is not running?", path)) return fmt.Errorf("Container %s is not running?", co)
return nil, false
} }
portC := render.ToContainerPorts(co.Ports) return nil
ports := strings.Split(portC, ",") }
if len(ports) == 0 {
func locateContainer(co string, cc []v1.Container) (*v1.Container, error) {
for i := range cc {
if cc[i].Name == co {
return &cc[i], nil
}
}
return nil, fmt.Errorf("unable to locate container named %q", co)
}
func (c *Container) listForwardable(path string) (port.ContainerPortSpecs, map[string]string, bool) {
po, err := fetchPod(c.App().factory, c.GetTable().Path)
if err != nil {
return nil, nil, false
}
co, err := locateContainer(path, po.Spec.Containers)
if err != nil {
c.App().Flash().Err(err)
return nil, nil, false
}
if err := checkRunningStatus(path, po.Status.ContainerStatuses); err != nil {
c.App().Flash().Err(err)
return nil, nil, false
}
exposedPorts := port.FromContainerPorts(path, co.Ports)
if len(exposedPorts) == 0 {
c.App().Flash().Err(errors.New("Container exposes no ports")) c.App().Flash().Err(errors.New("Container exposes no ports"))
return nil, false return nil, nil, false
} }
pp := make([]string, 0, len(ports)) return port.FromContainerPorts(path, co.Ports), po.Annotations, true
container, port, ok := parsePFAnn(po.Annotations[AnnDefaultPF])
if ok && container == path {
if index := indexOfPort(ports, port); index != -1 {
pp = append(pp, path+"/"+port)
ports = append(ports[:index], ports[index+1:]...)
}
}
for _, p := range ports {
if !isTCPPort(p) {
continue
}
pp = append(pp, path+"/"+p)
}
if len(pp) == 0 {
c.App().Flash().Err(errors.New("No TCP port available on container"))
return nil, false
}
return pp, true
}
func indexOfPort(pp []string, port string) int {
for i, p := range pp {
tokens := strings.Split(p, ":")
if len(tokens) == 2 {
if tokens[0] == port || tokens[1] == port {
return i
}
}
}
return -1
} }

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "os"
"path" "path"
"strings" "strings"
@ -90,7 +90,7 @@ func (d *Dir) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
yaml, err := ioutil.ReadFile(sel) yaml, err := os.ReadFile(sel)
if err != nil { if err != nil {
d.App().Flash().Err(err) d.App().Flash().Err(err)
return nil return nil
@ -157,7 +157,7 @@ func isKustomized(sel string) bool {
return false return false
} }
ff, err := ioutil.ReadDir(sel) ff, err := os.ReadDir(sel)
if err != nil { if err != nil {
return false return false
} }
@ -176,7 +176,7 @@ func containsDir(sel string) bool {
return false return false
} }
ff, err := ioutil.ReadDir(sel) ff, err := os.ReadDir(sel)
if err != nil { if err != nil {
return false return false
} }

View File

@ -45,13 +45,13 @@ func ShowDrain(view ResourceViewer, path string, defaults dao.DrainOptions, okFn
view.App().Flash().Clear() view.App().Flash().Clear()
opts.Timeout = a opts.Timeout = a
}) })
f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(v bool) { f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(_ string, v bool) {
opts.IgnoreAllDaemonSets = v opts.IgnoreAllDaemonSets = v
}) })
f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(v bool) { f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(_ string, v bool) {
opts.DeleteEmptyDirData = v opts.DeleteEmptyDirData = v
}) })
f.AddCheckbox("Force:", defaults.Force, func(v bool) { f.AddCheckbox("Force:", defaults.Force, func(_ string, v bool) {
opts.Force = v opts.Force = v
}) })

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@ -32,6 +33,7 @@ const (
type shellOpts struct { type shellOpts struct {
clear, background bool clear, background bool
pipes []string
binary string binary string
banner string banner string
args []string args []string
@ -96,40 +98,41 @@ func execute(opts shellOpts) error {
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer func() { defer func() {
cancel() if !opts.background {
clearScreen() cancel()
clearScreen()
}
}() }()
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() { go func(cancel context.CancelFunc) {
defer log.Debug().Msgf("SIGNAL_GOR - BAILED!!")
select { select {
case <-sigChan: case <-sigChan:
log.Debug().Msg("Command canceled with signal!") log.Debug().Msgf("Command canceled with signal!")
cancel() cancel()
case <-ctx.Done(): case <-ctx.Done():
return log.Debug().Msgf("SIGNAL Context CANCELED!")
} }
}() }(cancel)
log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " ")) cmds := make([]*exec.Cmd, 0, 1)
cmd := exec.Command(opts.binary, opts.args...) cmd := exec.CommandContext(ctx, opts.binary, opts.args...)
log.Debug().Msgf("RUNNING> %s", cmd)
cmds = append(cmds, cmd)
var err error for _, p := range opts.pipes {
if opts.background { tokens := strings.Split(p, " ")
err = cmd.Start() if len(tokens) < 2 {
} else { continue
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr }
_, _ = cmd.Stdout.Write([]byte(opts.banner)) cmd := exec.CommandContext(ctx, tokens[0], tokens[1:]...)
err = cmd.Run() log.Debug().Msgf("\t| %s", cmd)
cmds = append(cmds, cmd)
} }
select { return pipe(ctx, opts, cmds...)
case <-ctx.Done():
return errors.New("canceled by operator")
default:
return err
}
} }
func runKu(a *App, opts shellOpts) (string, error) { func runKu(a *App, opts shellOpts) (string, error) {
@ -358,3 +361,58 @@ func asResource(r config.Limits) v1.ResourceRequirements {
}, },
} }
} }
func pipe(ctx context.Context, opts shellOpts, cmds ...*exec.Cmd) error {
if len(cmds) == 0 {
return nil
}
if len(cmds) == 1 {
cmd := cmds[0]
if opts.background {
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, log.Logger, log.Logger
return cmd.Start()
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
//cmd.SysProcAttr = &syscall.SysProcAttr{
//// //Setpgid: true,
//// //Setctty: true,
// Foreground: true,
//}
_, _ = cmd.Stdout.Write([]byte(opts.banner))
log.Debug().Msgf("Running Start")
err := cmd.Run()
log.Debug().Msgf("Running Done")
return err
// select {
// case <-ctx.Done():
// return errors.New("canceled by operator")
// default:
// log.Debug().Msgf("PIPE RETURN %s", err)
// return err
// }
}
last := len(cmds) - 1
for i := 0; i < len(cmds); i++ {
cmds[i].Stderr = os.Stderr
if i+1 < len(cmds) {
r, w := io.Pipe()
cmds[i].Stdout, cmds[i+1].Stdin = w, r
}
}
cmds[last].Stdout = os.Stdout
for _, cmd := range cmds {
log.Debug().Msgf("Starting CMD %s", cmd)
if err := cmd.Start(); err != nil {
return err
}
}
log.Debug().Msgf("WAITING!!!")
err := cmds[len(cmds)-1].Wait()
log.Debug().Msgf("DONE WAITING!!!")
return err
}

View File

@ -132,7 +132,7 @@ func extractApp(ctx context.Context) (*App, error) {
// AsKey maps a string representation of a key to a tcell key. // AsKey maps a string representation of a key to a tcell key.
func asKey(key string) (tcell.Key, error) { func asKey(key string) (tcell.Key, error) {
for k, v := range tcell.KeyNames { for k, v := range tcell.KeyNames {
if v == key { if key == v {
return k, nil return k, nil
} }
} }

View File

@ -25,14 +25,14 @@ func TestParsePFAnn(t *testing.T) {
ok bool ok bool
}{ }{
"named-port": { "named-port": {
ann: "fred:blee", ann: "c1:blee",
co: "fred", co: "c1",
port: "blee", port: "blee",
ok: true, ok: true,
}, },
"port-num": { "port-num": {
ann: "fred:1234", ann: "c1:1234",
co: "fred", co: "c1",
port: "1234", port: "1234",
ok: true, ok: true,
}, },

View File

@ -1,14 +1,13 @@
package view package view
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"sync"
"time" "time"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
@ -25,7 +24,7 @@ import (
const ( const (
logTitle = "logs" logTitle = "logs"
logMessage = "Waiting for logs..." logMessage = "Waiting for logs...\n"
logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
flushTimeout = 1 * time.Millisecond flushTimeout = 1 * time.Millisecond
@ -35,11 +34,16 @@ const (
type Log struct { type Log struct {
*tview.Flex *tview.Flex
app *App app *App
logs *Logger logs *Logger
indicator *LogIndicator indicator *LogIndicator
ansiWriter io.Writer ansiWriter io.Writer
model *model.Log model *model.Log
cancelFn context.CancelFunc
cancelUpdates bool
mx sync.Mutex
logChan dao.LogChan
follow bool
} }
var _ model.Component = (*Log)(nil) var _ model.Component = (*Log)(nil)
@ -47,8 +51,10 @@ var _ model.Component = (*Log)(nil)
// NewLog returns a new viewer. // NewLog returns a new viewer.
func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log { func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log {
l := Log{ l := Log{
Flex: tview.NewFlex(), Flex: tview.NewFlex(),
model: model.NewLog(gvr, opts, flushTimeout), logChan: make(dao.LogChan, 2),
model: model.NewLog(gvr, opts, flushTimeout),
follow: true,
} }
return &l return &l
@ -76,21 +82,18 @@ func (l *Log) Init(ctx context.Context) (err error) {
return err return err
} }
l.logs.SetBorderPadding(0, 0, 1, 1) l.logs.SetBorderPadding(0, 0, 1, 1)
l.logs.SetText(logMessage) l.logs.SetText("[orange::d]" + logMessage)
l.logs.SetWrap(l.app.Config.K9s.Logger.TextWrap) l.logs.SetWrap(l.app.Config.K9s.Logger.TextWrap)
l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize) l.logs.SetMaxLines(l.app.Config.K9s.Logger.BufferSize)
l.logs.cmdBuff.AddListener(l)
l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String()) l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String())
l.AddItem(l.logs, 0, 1, true) l.AddItem(l.logs, 0, 1, true)
l.bindKeys() l.bindKeys()
l.StylesChanged(l.app.Styles) l.StylesChanged(l.app.Styles)
l.app.Styles.AddListener(l)
l.goFullScreen() l.goFullScreen()
l.model.Init(l.app.factory) l.model.Init(l.app.factory)
l.model.AddListener(l)
l.updateTitle() l.updateTitle()
l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime) l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime)
@ -103,6 +106,30 @@ func (l *Log) InCmdMode() bool {
return l.logs.cmdBuff.InCmdMode() return l.logs.cmdBuff.InCmdMode()
} }
// LogCanceled indicates no more logs are coming.
func (l *Log) LogCanceled() {
log.Debug().Msgf("LOGS_CANCELED!!!")
l.Flush([][]byte{[]byte("\n🏁 [red::b]Stream exited! No more logs...")})
}
// LogStop disables log flushes.
func (l *Log) LogStop() {
log.Debug().Msgf("LOG_STOP!!!")
l.mx.Lock()
defer l.mx.Unlock()
l.cancelUpdates = true
}
// LogResume resume log flushes.
func (l *Log) LogResume() {
l.mx.Lock()
defer l.mx.Unlock()
log.Debug().Msgf("LOG_RESUME!!!")
l.cancelUpdates = false
}
// LogCleared clears the logs. // LogCleared clears the logs.
func (l *Log) LogCleared() { func (l *Log) LogCleared() {
l.app.QueueUpdateDraw(func() { l.app.QueueUpdateDraw(func() {
@ -126,6 +153,9 @@ func (l *Log) LogFailed(err error) {
// LogChanged updates the logs. // LogChanged updates the logs.
func (l *Log) LogChanged(lines [][]byte) { func (l *Log) LogChanged(lines [][]byte) {
l.app.QueueUpdateDraw(func() { l.app.QueueUpdateDraw(func() {
if l.logs.GetText(true) == logMessage {
l.logs.Clear()
}
l.Flush(lines) l.Flush(lines)
}) })
} }
@ -166,15 +196,43 @@ func (l *Log) ExtraHints() map[string]string {
return nil return nil
} }
func (l *Log) getContext() context.Context {
if l.cancelFn != nil {
l.cancelFn()
}
ctx := context.Background()
ctx, l.cancelFn = context.WithCancel(ctx)
return ctx
}
// Start runs the component. // Start runs the component.
func (l *Log) Start() { func (l *Log) Start() {
l.model.Start() log.Debug().Msgf("LOG_VIEW STARTED!!")
l.model.Restart(l.getContext(), l.logChan, true)
l.model.AddListener(l)
l.app.Styles.AddListener(l)
l.logs.cmdBuff.AddListener(l)
l.logs.cmdBuff.AddListener(l.app.Prompt())
l.updateTitle()
} }
// Stop terminates the component. // Stop terminates the component.
func (l *Log) Stop() { func (l *Log) Stop() {
l.model.Stop() log.Debug().Msgf("LOG_VIEW STOPPED!")
l.model.RemoveListener(l) l.model.RemoveListener(l)
l.model.Stop()
log.Debug().Msgf("CLOSING LOG_CHANNEL!!!")
l.mx.Lock()
{
if l.cancelFn != nil {
l.cancelFn()
l.cancelFn = nil
}
close(l.logChan)
l.logChan = nil
}
l.mx.Unlock()
l.app.Styles.RemoveListener(l) l.app.Styles.RemoveListener(l)
l.logs.cmdBuff.RemoveListener(l) l.logs.cmdBuff.RemoveListener(l)
l.logs.cmdBuff.RemoveListener(l.app.Prompt()) l.logs.cmdBuff.RemoveListener(l.app.Prompt())
@ -185,12 +243,13 @@ func (l *Log) Name() string { return logTitle }
func (l *Log) bindKeys() { func (l *Log) bindKeys() {
l.logs.Actions().Set(ui.KeyActions{ l.logs.Actions().Set(ui.KeyActions{
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true), ui.Key0: ui.NewKeyAction("tail", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true), ui.Key1: ui.NewKeyAction("head", l.head(), true),
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true), ui.Key2: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true), ui.Key3: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true), ui.Key4: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), ui.Key5: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
ui.Key6: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true), ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true),
@ -242,13 +301,17 @@ func (l *Log) Indicator() *LogIndicator {
} }
func (l *Log) updateTitle() { func (l *Log) updateTitle() {
sinceSeconds, since := l.model.SinceSeconds(), "all" sinceSeconds, since := l.model.SinceSeconds(), "tail"
if sinceSeconds > 0 && sinceSeconds < 60*60 { if sinceSeconds > 0 && sinceSeconds < 60*60 {
since = fmt.Sprintf("%dm", sinceSeconds/60) since = fmt.Sprintf("%dm", sinceSeconds/60)
} }
if sinceSeconds >= 60*60 { if sinceSeconds >= 60*60 {
since = fmt.Sprintf("%dh", sinceSeconds/(60*60)) since = fmt.Sprintf("%dh", sinceSeconds/(60*60))
} }
if l.model.IsHead() {
since = "head"
}
var title string var title string
path, co := l.model.GetPath(), l.model.GetContainer() path, co := l.model.GetPath(), l.model.GetContainer()
if co == "" { if co == "" {
@ -274,25 +337,47 @@ var EOL = []byte{'\n'}
// Flush write logs to viewer. // Flush write logs to viewer.
func (l *Log) Flush(lines [][]byte) { func (l *Log) Flush(lines [][]byte) {
log.Debug().Msgf("LINES [%d]%d", runtime.NumGoroutine(), len(strings.Split(l.logs.GetText(true), "\n"))) defer func() {
if !l.indicator.AutoScroll() { if l.cancelUpdates {
l.cancelUpdates = false
}
}()
if len(lines) == 0 || !l.indicator.AutoScroll() || l.cancelUpdates {
return return
} }
_, _ = l.ansiWriter.Write(EOL) for i := 0; i < len(lines); i++ {
if _, err := l.ansiWriter.Write(bytes.Join(lines, EOL)); err != nil { if l.cancelUpdates {
log.Error().Err(err).Msgf("write logs failed") break
}
_, _ = l.ansiWriter.Write(lines[i])
}
if l.follow {
l.logs.ScrollToEnd()
} }
l.logs.ScrollToEnd()
l.indicator.Refresh()
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Actions()... // Actions()...
func (l *Log) head() func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("!!!!HEAD!!!!")
l.cancelUpdates = true
l.logs.Clear()
l.model.Head(l.getContext(), l.logChan)
l.updateTitle()
return nil
}
}
func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey {
l.model.SetSinceSeconds(int64(a)) l.logs.Clear()
l.model.SetSinceSeconds(l.getContext(), l.logChan, int64(a))
l.updateTitle() l.updateTitle()
return nil return nil
} }
} }
@ -302,7 +387,7 @@ func (l *Log) toggleAllContainers(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
l.indicator.ToggleAllContainers() l.indicator.ToggleAllContainers()
l.model.ToggleAllContainers() l.model.ToggleAllContainers(l.getContext(), l.logChan)
l.updateTitle() l.updateTitle()
return nil return nil
@ -322,7 +407,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
// SaveCmd dumps the logs to file. // SaveCmd dumps the logs to file.
func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey {
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil { if path, err := saveData(l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true)); err != nil {
l.app.Flash().Err(err) l.app.Flash().Err(err)
} else { } else {
l.app.Flash().Infof("Log %s saved successfully!", path) l.app.Flash().Infof("Log %s saved successfully!", path)
@ -377,7 +462,9 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey { func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey {
_, _, w, _ := l.GetRect() _, _, w, _ := l.GetRect()
fmt.Fprintf(l.ansiWriter, "\n[white::b]%s[::]", strings.Repeat("─", w-4)) fmt.Fprintf(l.ansiWriter, "\n[white:-:b]%s[-:-:-]", strings.Repeat("─", w-4))
l.follow = true
return nil return nil
} }
@ -388,6 +475,7 @@ func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
l.indicator.ToggleTimestamp() l.indicator.ToggleTimestamp()
l.model.ToggleShowTimestamp(l.indicator.showTime) l.model.ToggleShowTimestamp(l.indicator.showTime)
l.indicator.Refresh()
return nil return nil
} }
@ -399,6 +487,8 @@ func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey {
l.indicator.ToggleTextWrap() l.indicator.ToggleTextWrap()
l.logs.SetWrap(l.indicator.textWrap) l.logs.SetWrap(l.indicator.textWrap)
l.indicator.Refresh()
return nil return nil
} }
@ -409,11 +499,15 @@ func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
l.indicator.ToggleAutoScroll() l.indicator.ToggleAutoScroll()
if l.indicator.AutoScroll() { l.follow = l.indicator.AutoScroll()
l.model.Start() // if l.indicator.AutoScroll() {
} else {
l.model.Stop() // // l.model.Restart(l.getContext(), l.logChan, false)
} // } else {
// // l.model.Stop()
// }
l.indicator.Refresh()
return nil return nil
} }
@ -423,6 +517,8 @@ func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
l.indicator.ToggleFullScreen() l.indicator.ToggleFullScreen()
l.goFullScreen() l.goFullScreen()
l.indicator.Refresh()
return nil return nil
} }

View File

@ -7,17 +7,7 @@ import (
"github.com/derailed/tview" "github.com/derailed/tview"
) )
const ( const spacer = " "
autoscroll = "Autoscroll"
fullscreen = "FullScreen"
timestamp = "Timestamps"
wrap = "Wrap"
allContainers = "AllContainers"
on = "[limegreen::]On"
off = "[gray::]Off"
spacer = " "
bold = "[-::b]"
)
// LogIndicator represents a log view indicator. // LogIndicator represents a log view indicator.
type LogIndicator struct { type LogIndicator struct {
@ -25,6 +15,7 @@ type LogIndicator struct {
styles *config.Styles styles *config.Styles
scrollStatus int32 scrollStatus int32
indicator []byte
fullScreen bool fullScreen bool
textWrap bool textWrap bool
showTime bool showTime bool
@ -33,15 +24,16 @@ type LogIndicator struct {
} }
// NewLogIndicator returns a new indicator. // NewLogIndicator returns a new indicator.
func NewLogIndicator(cfg *config.Config, styles *config.Styles, isContainerLogView bool) *LogIndicator { func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bool) *LogIndicator {
l := LogIndicator{ l := LogIndicator{
styles: styles, styles: styles,
TextView: tview.NewTextView(), TextView: tview.NewTextView(),
indicator: make([]byte, 0, 100),
scrollStatus: 1, scrollStatus: 1,
fullScreen: cfg.K9s.Logger.FullScreenLogs, fullScreen: cfg.K9s.Logger.FullScreenLogs,
textWrap: cfg.K9s.Logger.TextWrap, textWrap: cfg.K9s.Logger.TextWrap,
showTime: cfg.K9s.Logger.ShowTime, showTime: cfg.K9s.Logger.ShowTime,
shouldDisplayAllContainers: isContainerLogView, shouldDisplayAllContainers: allContainers,
} }
l.StylesChanged(styles) l.StylesChanged(styles)
styles.AddListener(&l) styles.AddListener(&l)
@ -110,24 +102,46 @@ func (l *LogIndicator) ToggleAllContainers() {
l.Refresh() l.Refresh()
} }
// Refresh updates the view. func (l *LogIndicator) reset() {
func (l *LogIndicator) Refresh() {
l.Clear() l.Clear()
if l.shouldDisplayAllContainers { l.indicator = l.indicator[:0]
l.update(allContainers, l.allContainers, spacer)
}
l.update(autoscroll, l.AutoScroll(), spacer)
l.update(fullscreen, l.fullScreen, spacer)
l.update(timestamp, l.showTime, spacer)
l.update(wrap, l.textWrap, "")
} }
func (l *LogIndicator) update(title string, state bool, padding string) { // Refresh updates the view.
bb := []byte(bold + title + ":") func (l *LogIndicator) Refresh() {
if state { l.reset()
bb = append(bb, []byte(on)...)
} else { if l.shouldDisplayAllContainers {
bb = append(bb, []byte(off)...) if l.allContainers {
l.indicator = append(l.indicator, "[::b]AllContainers:[limegreen::b]On[-::]"+spacer...)
} else {
l.indicator = append(l.indicator, "[::b]AllContainers:[gray::d]Off[-::]"+spacer...)
}
} }
_, _ = l.Write(append(bb, []byte(padding)...))
if l.AutoScroll() {
l.indicator = append(l.indicator, "[::b]Autoscroll:[limegreen::b]On[-::]"+spacer...)
} else {
l.indicator = append(l.indicator, "[::b]Autoscroll:[gray::d]Off[-::]"+spacer...)
}
if l.FullScreen() {
l.indicator = append(l.indicator, "[::b]FullScreen:[limegreen::b]On[-::]"+spacer...)
} else {
l.indicator = append(l.indicator, "[::b]FullScreen:[gray::d]Off[-::]"+spacer...)
}
if l.Timestamp() {
l.indicator = append(l.indicator, "[::b]Timestamps:[limegreen::b]On[-::]"+spacer...)
} else {
l.indicator = append(l.indicator, "[::b]Timestamps:[gray::d]Off[-::]"+spacer...)
}
if l.TextWrap() {
l.indicator = append(l.indicator, "[::b]Wrap:[limegreen::b]On[-::]"...)
} else {
l.indicator = append(l.indicator, "[::b]Wrap:[gray::d]Off[-::]"...)
}
_, _ = l.Write(l.indicator)
} }

View File

@ -14,11 +14,11 @@ func TestLogIndicatorRefresh(t *testing.T) {
li *view.LogIndicator li *view.LogIndicator
e string e string
}{ }{
"all containers": { "all-containers": {
view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[-::b]AllContainers:[gray::]Off [-::b]Autoscroll:[limegreen::]On [-::b]FullScreen:[gray::]Off [-::b]Timestamps:[gray::]Off [-::b]Wrap:[gray::]Off\n", view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
}, },
"no all containers": { "plain": {
view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[-::b]Autoscroll:[limegreen::]On [-::b]FullScreen:[gray::]Off [-::b]Timestamps:[gray::]Off [-::b]Wrap:[gray::]Off\n", view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
}, },
} }
@ -26,7 +26,18 @@ func TestLogIndicatorRefresh(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
u.li.Refresh() u.li.Refresh()
assert.Equal(t, u.li.GetText(false), u.e) assert.Equal(t, u.e, u.li.GetText(false))
}) })
} }
} }
func BenchmarkLogIndicatorRefresh(b *testing.B) {
defaults := config.NewStyles()
v := view.NewLogIndicator(config.NewConfig(nil), defaults, true)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
v.Refresh()
}
}

View File

@ -23,7 +23,7 @@ func TestLogAutoScroll(t *testing.T) {
v.GetModel().Set(ii) v.GetModel().Set(ii)
v.GetModel().Notify() v.GetModel().Notify()
assert.Equal(t, 15, len(v.Hints())) assert.Equal(t, 16, len(v.Hints()))
v.toggleAutoScrollCmd(nil) v.toggleAutoScrollCmd(nil)
assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true)) assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true))
@ -75,8 +75,7 @@ func TestLogTimestamp(t *testing.T) {
&dao.LogItem{ &dao.LogItem{
Pod: "fred/blee", Pod: "fred/blee",
Container: "c1", Container: "c1",
Timestamp: "ttt", Bytes: []byte("ttt Testing 1, 2, 3\n"),
Bytes: []byte("Testing 1, 2, 3"),
}, },
) )
var list logList var list logList
@ -84,9 +83,11 @@ func TestLogTimestamp(t *testing.T) {
l.GetModel().Set(ii) l.GetModel().Set(ii)
l.SendKeys(ui.KeyT) l.SendKeys(ui.KeyT)
l.Logs().Clear() l.Logs().Clear()
l.Flush(ii.Lines(true)) ll := make([][]byte, ii.Len())
ii.Lines(0, true, ll)
l.Flush(ll)
assert.Equal(t, fmt.Sprintf("\n%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3"), l.Logs().GetText(true)) assert.Equal(t, fmt.Sprintf("%-30s %s", "ttt", "fred/blee c1 Testing 1, 2, 3\n"), l.Logs().GetText(true))
assert.Equal(t, 2, list.change) assert.Equal(t, 2, list.change)
assert.Equal(t, 2, list.clear) assert.Equal(t, 2, list.clear)
assert.Equal(t, 0, list.fail) assert.Equal(t, 0, list.fail)
@ -131,5 +132,8 @@ func (l *logList) LogChanged(ll [][]byte) {
l.lines += string(line) l.lines += string(line)
} }
} }
func (l *logList) LogCanceled() {}
func (l *logList) LogStop() {}
func (l *logList) LogResume() {}
func (l *logList) LogCleared() { l.clear++ } func (l *logList) LogCleared() { l.clear++ }
func (l *logList) LogFailed(error) { l.fail++ } func (l *logList) LogFailed(error) { l.fail++ }

View File

@ -3,7 +3,7 @@ package view_test
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -25,10 +25,32 @@ func TestLog(t *testing.T) {
v.Init(makeContext()) v.Init(makeContext())
ii := dao.NewLogItems() ii := dao.NewLogItems()
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")) ii.Add(dao.NewLogItemFromString("blee\n"), dao.NewLogItemFromString("bozo\n"))
v.Flush(ii.Lines(false)) ll := make([][]byte, ii.Len())
ii.Lines(0, false, ll)
v.Flush(ll)
assert.Equal(t, 29, len(v.Logs().GetText(true))) assert.Equal(t, "Waiting for logs...\nblee\nbozo\n", v.Logs().GetText(true))
}
func TestLogFlush(t *testing.T) {
opts := dao.LogOptions{
Path: "fred/p1",
Container: "blee",
}
v := view.NewLog(client.NewGVR("v1/pods"), &opts)
v.Init(makeContext())
items := dao.NewLogItems()
items.Add(
dao.NewLogItemFromString("\033[0;30mblee\n"),
dao.NewLogItemFromString("\033[0;32mBozo\n"),
)
ll := make([][]byte, items.Len())
items.Lines(0, false, ll)
v.Flush(ll)
assert.Equal(t, "[orange::d]Waiting for logs...\n[black:]blee\n[green:]Bozo\n\n", v.Logs().GetText(false))
} }
func BenchmarkLogFlush(b *testing.B) { func BenchmarkLogFlush(b *testing.B) {
@ -41,13 +63,17 @@ func BenchmarkLogFlush(b *testing.B) {
items := dao.NewLogItems() items := dao.NewLogItems()
items.Add( items.Add(
dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("\033[0;30mblee\n"),
dao.NewLogItemFromString("bozo"), dao.NewLogItemFromString("\033[0;101mBozo\n"),
dao.NewLogItemFromString("\033[0;101mBozo\n"),
) )
ll := make([][]byte, items.Len())
items.Lines(0, false, ll)
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
v.Flush(items.Lines(false)) v.Flush(ll)
} }
} }
@ -76,12 +102,15 @@ func TestLogViewSave(t *testing.T) {
app := makeApp() app := makeApp()
ii := dao.NewLogItems() ii := dao.NewLogItems()
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")) ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo"))
v.Flush(ii.Lines(false)) ll := make([][]byte, ii.Len())
ii.Lines(0, false, ll)
v.Flush(ll)
config.K9sDumpDir = "/tmp" config.K9sDumpDir = "/tmp"
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir) c1, _ := os.ReadDir(dir)
v.SaveCmd(nil) v.SaveCmd(nil)
c2, _ := ioutil.ReadDir(dir) c2, _ := os.ReadDir(dir)
assert.Equal(t, len(c2), len(c1)+1) assert.Equal(t, len(c2), len(c1)+1)
} }

View File

@ -59,8 +59,7 @@ func (l *Logger) Init(_ context.Context) error {
func (l *Logger) BufferChanged(s string) {} func (l *Logger) BufferChanged(s string) {}
// BufferCompleted indicates input was accepted. // BufferCompleted indicates input was accepted.
func (l *Logger) BufferCompleted(s string) { func (l *Logger) BufferCompleted(s string) {}
}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (l *Logger) BufferActive(state bool, k model.BufferKind) { func (l *Logger) BufferActive(state bool, k model.BufferKind) {

View File

@ -3,6 +3,7 @@ package view
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"time" "time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -138,19 +139,35 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
path := p.GetTable().GetSelectedItem() selections := p.GetTable().GetSelectedItems()
if path == "" { if len(selections) == 0 {
return nil return evt
} }
showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", path), func() { p.Stop()
var pf dao.PortForward defer p.Start()
pf.Init(p.App().factory, client.NewGVR("portforwards")) var msg string
if err := pf.Delete(path, true, true); err != nil { if len(selections) > 1 {
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), p.GVR())
} else {
h, err := pfToHuman(selections[0])
if err == nil {
msg = fmt.Sprintf("Delete %s %s?", p.GVR().R(), h)
} else {
p.App().Flash().Err(err) p.App().Flash().Err(err)
return return nil
} }
p.App().Flash().Infof("PortForward %s deleted!", path) }
showModal(p.App().Content.Pages, msg, func() {
for _, s := range selections {
var pf dao.PortForward
pf.Init(p.App().factory, client.NewGVR("portforwards"))
if err := pf.Delete(s, true, true); err != nil {
p.App().Flash().Err(err)
return
}
}
p.App().Flash().Infof("Successfully deleted %d PortForward!", len(selections))
p.GetTable().Refresh() p.GetTable().Refresh()
}) })
@ -160,6 +177,16 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
var selRx = regexp.MustCompile(`\A([\w-]+)/([\w-]+)\|([\w-]+)\|(\d+):(\d+)`)
func pfToHuman(s string) (string, error) {
mm := selRx.FindStringSubmatch(s)
if len(mm) < 6 {
return "", fmt.Errorf("Unable to parse selection %s", s)
}
return fmt.Sprintf("%s::%s %s->%s", mm[2], mm[3], mm[4], mm[5]), nil
}
func showModal(p *ui.Pages, msg string, ok func()) { func showModal(p *ui.Pages, msg string, ok func()) {
m := tview.NewModal(). m := tview.NewModal().
AddButtons([]string{"Cancel", "OK"}). AddButtons([]string{"Cancel", "OK"}).

View File

@ -7,18 +7,20 @@ import (
"strings" "strings"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/port"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/rs/zerolog/log"
) )
const portForwardKey = "portforward" const portForwardKey = "portforward"
// PortForwardCB represents a port-forward callback function. // PortForwardCB represents a port-forward callback function.
type PortForwardCB func(v ResourceViewer, path, co string, mapper []client.PortTunnel) type PortForwardCB func(ResourceViewer, string, port.PortTunnels) error
// ShowPortForwards pops a port forwarding configuration dialog. // ShowPortForwards pops a port forwarding configuration dialog.
func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string, okFn PortForwardCB) { func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpecs, aa port.Annotations, okFn PortForwardCB) {
styles := v.App().Styles.Dialog() styles := v.App().Styles.Dialog()
f := tview.NewForm() f := tview.NewForm()
@ -32,37 +34,28 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string,
address := v.App().Config.CurrentCluster().PortForwardAddress address := v.App().Config.CurrentCluster().PortForwardAddress
var p1, p2 string pf, err := aa.PreferredPorts(ports)
if len(ports) > 0 { if err != nil {
p1, p2 = ports[0], extractPort(ports[0]) log.Warn().Err(err).Msgf("unable to resolve ports")
if len(ann) != 0 {
container, port, ok := parsePFAnn(ann)
if ok {
for _, p := range ports {
co, po, portNum := parsePort(p)
if co == container && port == po || port == portNum {
p1, p2 = p, extractPort(p)
break
}
}
}
}
} }
p1, p2 := pf.ToPortSpec(ports)
fieldLen := int(math.Max(30, float64(len(p1)))) fieldLen := int(math.Max(30, float64(len(p1))))
f.AddInputField("Container Port:", p1, fieldLen, nil, func(p string) { f.AddInputField("Container Port:", p1, fieldLen, nil, nil)
p1 = p coField := f.GetFormItemByLabel("Container Port:").(*tview.InputField)
}) if coField.GetText() == "" {
field := f.GetFormItemByLabel("Container Port:").(*tview.InputField) coField.SetPlaceholder("Enter a container name/port")
if field.GetText() == "" {
field.SetPlaceholder("Enter a container name/port")
} }
f.AddInputField("Local Port:", p2, fieldLen, nil, func(p string) { f.AddInputField("Local Port:", p2, fieldLen, nil, nil)
p2 = p poField := f.GetFormItemByLabel("Local Port:").(*tview.InputField)
}) if poField.GetText() == "" {
field = f.GetFormItemByLabel("Local Port:").(*tview.InputField) poField.SetPlaceholder("Enter a local port")
if field.GetText() == "" {
field.SetPlaceholder("Enter a local port")
} }
coField.SetChangedFunc(func(s string) {
port := extractPort(s)
poField.SetText(port)
p2 = port
})
f.AddInputField("Address:", address, fieldLen, nil, func(h string) { f.AddInputField("Address:", address, fieldLen, nil, func(h string) {
address = h address = h
}) })
@ -76,21 +69,18 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string,
} }
f.AddButton("OK", func() { f.AddButton("OK", func() {
pp1 := strings.Split(p1, ",") if coField.GetText() == "" || poField.GetText() == "" {
pp2 := strings.Split(p2, ",")
if len(pp1) == 0 || len(pp1) != len(pp2) {
v.App().Flash().Err(fmt.Errorf("container to local port mismatch")) v.App().Flash().Err(fmt.Errorf("container to local port mismatch"))
return return
} }
var tt []client.PortTunnel tt, err := port.ToTunnels(address, coField.GetText(), poField.GetText())
for i := range pp1 { if err != nil {
tt = append(tt, client.PortTunnel{ v.App().Flash().Err(err)
Address: address, return
LocalPort: pp2[i], }
ContainerPort: extractPort(pp1[i]), if err := okFn(v, path, tt); err != nil {
}) v.App().Flash().Err(err)
} }
okFn(v, path, extractContainer(pp1[0]), tt)
}) })
pages := v.App().Content.Pages pages := v.App().Content.Pages
f.AddButton("Cancel", func() { f.AddButton("Cancel", func() {
@ -108,7 +98,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string,
modal := tview.NewModalForm("<PortForward>", f) modal := tview.NewModalForm("<PortForward>", f)
msg := path msg := path
if len(ports) > 1 { if len(ports) > 1 {
msg += "\n\nExposed Ports:\n" + strings.Join(ports, "\n") msg += "\n\nExposed Ports:\n" + ports.Dump()
} }
modal.SetText(msg) modal.SetText(msg)
modal.SetTextColor(styles.FgColor.Color()) modal.SetTextColor(styles.FgColor.Color())
@ -131,16 +121,6 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func parsePort(p string) (string, string, string) {
rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(UDP)?\z`)
mm := rx.FindStringSubmatch(p)
if len(mm) != 5 {
return "", "", ""
}
return mm[1], mm[2], mm[3]
}
func extractPort(p string) string { func extractPort(p string) string {
rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(UDP)?\z`) rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(UDP)?\z`)
mm := rx.FindStringSubmatch(p) mm := rx.FindStringSubmatch(p)

View File

@ -6,44 +6,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestExtractPort(t *testing.T) {
uu := map[string]struct {
port, e string
}{
"empty": {
"", "",
},
"full": {
"co/fred:8000", "8000",
},
"named": {
"fred:8000", "8000",
},
"port": {
"8000", "8000",
},
"protocol": {
"dns:53UDP", "53",
},
"unamed": {
"dns/53", "53",
},
"pod-dashed": {
"blee-fred/:5000", "5000",
},
"co-dashed": {
"blee/fred-doh:5000", "5000",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, extractPort(u.port))
})
}
}
func TestExtractContainer(t *testing.T) { func TestExtractContainer(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
port, e string port, e string

View File

@ -1,13 +1,10 @@
package view package view
import ( import (
"errors"
"fmt" "fmt"
"net"
"strconv"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/port"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/watch" "github.com/derailed/k9s/internal/watch"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -19,8 +16,6 @@ import (
"k8s.io/client-go/tools/portforward" "k8s.io/client-go/tools/portforward"
) )
const AnnDefaultPF = "k9s.imhotep.io/default-portforward-container"
// PortForwardExtender adds port-forward extensions. // PortForwardExtender adds port-forward extensions.
type PortForwardExtender struct { type PortForwardExtender struct {
ResourceViewer ResourceViewer
@ -78,19 +73,11 @@ func (p *PortForwardExtender) fetchPodName(path string) (string, error) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func tryListenPort(address, port string) error {
server, err := net.Listen("tcp", fmt.Sprintf("%s:%s", address, port))
if err != nil {
return err
}
return server.Close()
}
func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForwarder) { func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForwarder) {
v.App().factory.AddForwarder(pf) v.App().factory.AddForwarder(pf)
v.App().QueueUpdateDraw(func() { v.App().QueueUpdateDraw(func() {
v.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) v.App().Flash().Infof("PortForward activated %s", pf.ID())
DismissPortForwards(v, v.App().Content.Pages) DismissPortForwards(v, v.App().Content.Pages)
}) })
@ -106,67 +93,77 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward
}) })
} }
func startFwdCB(v ResourceViewer, path, co string, tt []client.PortTunnel) { func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error {
for _, t := range tt { if err := pts.CheckAvailable(); err != nil {
err := tryListenPort(t.Address, t.LocalPort) return err
if err != nil { }
v.App().Flash().Err(err)
return for _, pt := range pts {
if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, pt.Container, pt.PortMap())); ok {
return fmt.Errorf("A port-forward is already active on pod %s", path)
} }
pf := dao.NewPortForwarder(v.App().factory)
fwd, err := pf.Start(path, pt)
if err != nil {
return err
}
log.Debug().Msgf(">>> Starting port forward %q -- %#v", pf.ID(), pt)
go runForward(v, pf, fwd)
} }
if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, co)); ok { return nil
v.App().Flash().Err(errors.New("A port-forward is already active on this pod"))
return
}
pf := dao.NewPortForwarder(v.App().factory)
fwd, err := pf.Start(path, co, tt)
if err != nil {
v.App().Flash().Err(err)
return
}
log.Debug().Msgf(">>> Starting port forward %q %#v", path, tt)
go runForward(v, pf, fwd)
} }
func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
mm, coPort, err := fetchPodPorts(v.App().factory, path) mm, anns, err := fetchPodPorts(v.App().factory, path)
if err != nil { if err != nil {
return err return err
} }
ports := make([]string, 0, len(mm)) ports := make(port.ContainerPortSpecs, 0, len(mm))
for co, pp := range mm { for co, pp := range mm {
for _, p := range pp { for _, p := range pp {
if p.Protocol != v1.ProtocolTCP { if p.Protocol != v1.ProtocolTCP {
continue continue
} }
ports = append(ports, client.FQN(co, p.Name)+":"+strconv.Itoa(int(p.ContainerPort))) ports = append(ports, port.NewPortSpec(co, p.Name, p.ContainerPort))
} }
} }
ShowPortForwards(v, path, ports, coPort, cb) if spec, ok := anns[port.K9sAutoPortForwardsKey]; ok {
pfs, err := port.ParsePFs(spec)
if err != nil {
return err
}
pts, err := pfs.ToTunnels(v.App().Config.CurrentCluster().PortForwardAddress, ports, port.IsPortFree)
if err != nil {
return err
}
return startFwdCB(v, path, pts)
}
ShowPortForwards(v, path, ports, anns, cb)
return nil return nil
} }
func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, string, error) { func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, map[string]string, error) {
log.Debug().Msgf("Fetching ports on pod %q", path) log.Debug().Msgf("Fetching ports on pod %q", path)
o, err := f.Get("v1/pods", path, true, labels.Everything()) o, err := f.Get("v1/pods", path, true, labels.Everything())
if err != nil { if err != nil {
return nil, "", err return nil, nil, err
} }
var pod v1.Pod var pod v1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
if err != nil { if err != nil {
return nil, "", err return nil, nil, err
} }
pp := make(map[string][]v1.ContainerPort) pp := make(map[string][]v1.ContainerPort, len(pod.Spec.Containers))
for _, co := range pod.Spec.Containers { for _, co := range pod.Spec.Containers {
pp[co.Name] = co.Ports pp[co.Name] = co.Ports
} }
return pp, pod.Annotations[AnnDefaultPF], nil return pp, pod.Annotations, nil
} }

View File

@ -176,7 +176,6 @@ func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey {
p.App().Flash().Infof("Delete resource %s %s", p.GVR(), selections[0]) p.App().Flash().Infof("Delete resource %s %s", p.GVR(), selections[0])
} }
p.GetTable().ShowDeleted() p.GetTable().ShowDeleted()
log.Debug().Msgf("SELS %v", selections)
for _, path := range selections { for _, path := range selections {
if err := nuker.Delete(path, true, true); err != nil { if err := nuker.Delete(path, true, true); err != nil {
p.App().Flash().Errf("Delete failed with %s", err) p.App().Flash().Errf("Delete failed with %s", err)

View File

@ -36,7 +36,7 @@ func (r *RestartExtender) bindKeys(aa ui.KeyActions) {
func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
paths := r.GetTable().GetSelectedItems() paths := r.GetTable().GetSelectedItems()
if len(paths) == 0 { if len(paths) == 0 || paths[0] == "" {
return nil return nil
} }

View File

@ -36,26 +36,30 @@ func (s *ScaleExtender) bindKeys(aa ui.KeyActions) {
} }
func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey {
path := s.GetTable().GetSelectedItem() paths := s.GetTable().GetSelectedItems()
if path == "" { if len(paths) == 0 {
return nil return nil
} }
s.Stop() s.Stop()
defer s.Start() defer s.Start()
s.showScaleDialog(path) s.showScaleDialog(paths)
return nil return nil
} }
func (s *ScaleExtender) showScaleDialog(path string) { func (s *ScaleExtender) showScaleDialog(paths []string) {
form, err := s.makeScaleForm(path) form, err := s.makeScaleForm(paths)
if err != nil { if err != nil {
s.App().Flash().Err(err) s.App().Flash().Err(err)
return return
} }
confirm := tview.NewModalForm("<Scale>", form) confirm := tview.NewModalForm("<Scale>", form)
confirm.SetText(fmt.Sprintf("Scale %s %s", s.GVR(), path)) msg := fmt.Sprintf("Scale %s %s?", s.GVR().R(), paths[0])
if len(paths) > 1 {
msg = fmt.Sprintf("Scale [%d] %s?", len(paths), s.GVR().R())
}
confirm.SetText(msg)
confirm.SetDoneFunc(func(int, string) { confirm.SetDoneFunc(func(int, string) {
s.dismissDialog() s.dismissDialog()
}) })
@ -71,40 +75,49 @@ func (s *ScaleExtender) valueOf(col string) (string, error) {
return s.GetTable().GetSelectedCell(colIdx), nil return s.GetTable().GetSelectedCell(colIdx), nil
} }
func (s *ScaleExtender) makeScaleForm(sel string) (*tview.Form, error) { func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) {
f := s.makeStyledForm() f := s.makeStyledForm()
replicas, err := s.valueOf("READY") factor := "0"
if err != nil { if len(sels) == 1 {
return nil, err replicas, err := s.valueOf("READY")
if err != nil {
return nil, err
}
tokens := strings.Split(replicas, "/")
if len(tokens) < 2 {
return nil, fmt.Errorf("unable to locate replicas from %s", replicas)
}
factor = strings.TrimRight(tokens[1], ui.DeltaSign)
} }
tokens := strings.Split(replicas, "/") f.AddInputField("Replicas:", factor, 4, func(textToCheck string, lastChar rune) bool {
if len(tokens) < 2 {
return nil, fmt.Errorf("unable to locate replicas from %s", replicas)
}
replicas = strings.TrimRight(tokens[1], ui.DeltaSign)
f.AddInputField("Replicas:", replicas, 4, func(textToCheck string, lastChar rune) bool {
_, err := strconv.Atoi(textToCheck) _, err := strconv.Atoi(textToCheck)
return err == nil return err == nil
}, func(changed string) { }, func(changed string) {
replicas = changed factor = changed
}) })
f.AddButton("OK", func() { f.AddButton("OK", func() {
defer s.dismissDialog() defer s.dismissDialog()
count, err := strconv.Atoi(replicas) count, err := strconv.Atoi(factor)
if err != nil { if err != nil {
s.App().Flash().Err(err) s.App().Flash().Err(err)
return return
} }
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout()) ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
defer cancel() defer cancel()
if err := s.scale(ctx, sel, count); err != nil { for _, sel := range sels {
log.Error().Err(err).Msgf("DP %s scaling failed", sel) if err := s.scale(ctx, sel, count); err != nil {
s.App().Flash().Err(err) log.Error().Err(err).Msgf("DP %s scaling failed", sel)
return s.App().Flash().Err(err)
return
}
}
if len(sels) == 1 {
s.App().Flash().Infof("[%d] %s scaled successfully", len(sels), s.GVR().R())
} else {
s.App().Flash().Infof("%s %s scaled successfully", s.GVR().R(), sels[0])
} }
s.App().Flash().Infof("Resource %s:%s scaled successfully", s.GVR(), sel)
}) })
f.AddButton("Cancel", func() { f.AddButton("Cancel", func() {

View File

@ -2,6 +2,7 @@ package view
import ( import (
"context" "context"
"strings"
"time" "time"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
@ -53,9 +54,17 @@ func (t *Table) Init(ctx context.Context) (err error) {
} }
// HeaderIndex returns index of a given column or false if not found. // HeaderIndex returns index of a given column or false if not found.
func (t *Table) HeaderIndex(header string) (int, bool) { func (t *Table) HeaderIndex(colName string) (int, bool) {
for i := 0; i < t.GetColumnCount(); i++ { for i := 0; i < t.GetColumnCount(); i++ {
if h := t.GetCell(0, i); h != nil && h.Text == header { h := t.GetCell(0, i)
if h == nil {
continue
}
s := h.Text
if idx := strings.Index(s, "["); idx > 0 {
s = s[:idx]
}
if s == colName {
return i, true return i, true
} }
} }

View File

@ -2,7 +2,7 @@ package view
import ( import (
"context" "context"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
@ -25,10 +25,10 @@ func TestTableSave(t *testing.T) {
v.SetTitle("k9s-test") v.SetTitle("k9s-test")
dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir) c1, _ := os.ReadDir(dir)
v.saveCmd(nil) v.saveCmd(nil)
c2, _ := ioutil.ReadDir(dir) c2, _ := os.ReadDir(dir)
assert.Equal(t, len(c2), len(c1)+1) assert.Equal(t, len(c2), len(c1)+1)
} }

View File

@ -244,11 +244,8 @@ func (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, err
func (f *Factory) AddForwarder(pf Forwarder) { func (f *Factory) AddForwarder(pf Forwarder) {
f.mx.Lock() f.mx.Lock()
defer f.mx.Unlock() defer f.mx.Unlock()
f.forwarders[pf.Path()] = pf
for k, v := range f.forwarders { f.forwarders[pf.Path()] = pf
log.Debug().Msgf("%q -- %#v", k, v)
}
} }
// DeleteForwarder deletes portforward for a given container. // DeleteForwarder deletes portforward for a given container.
@ -277,11 +274,20 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) {
return fwd, ok return fwd, ok
} }
// BOZO!! Review!!!
// ValidatePortForwards check if pods are still around for portforwards. // ValidatePortForwards check if pods are still around for portforwards.
func (f *Factory) ValidatePortForwards() { func (f *Factory) ValidatePortForwards() {
for k, fwd := range f.forwarders { for k, fwd := range f.forwarders {
tokens := strings.Split(k, ":") tokens := strings.Split(k, ":")
_, err := f.Get("v1/pods", tokens[0], false, labels.Everything()) if len(tokens) != 2 {
log.Error().Msgf("Invalid fwd keys %q", k)
return
}
paths := strings.Split(tokens[0], "|")
if len(paths) < 1 {
log.Error().Msgf("Invalid path %q", tokens[0])
}
_, err := f.Get("v1/pods", paths[0], false, labels.Everything())
if err != nil { if err != nil {
fwd.Stop() fwd.Stop()
delete(f.forwarders, k) delete(f.forwarders, k)

View File

@ -3,7 +3,7 @@ package watch
import ( import (
"strings" "strings"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/port"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"k8s.io/client-go/tools/portforward" "k8s.io/client-go/tools/portforward"
) )
@ -11,19 +11,22 @@ import (
// Forwarder represents a port forwarder. // Forwarder represents a port forwarder.
type Forwarder interface { type Forwarder interface {
// Start starts a port-forward. // Start starts a port-forward.
Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) Start(path string, tunnel port.PortTunnel) (*portforward.PortForwarder, error)
// Stop terminates a port forward. // Stop terminates a port forward.
Stop() Stop()
// ID returns the pf id.
ID() string
// Path returns a resource FQN. // Path returns a resource FQN.
Path() string Path() string
// Container returns a container name. // Container returns a container name.
Container() string Container() string
// Ports returns container exposed ports. // Ports returns the port mapping.
Ports() []string Port() string
// FQN returns the full port-forward name. // FQN returns the full port-forward name.
FQN() string FQN() string
@ -49,10 +52,11 @@ func NewForwarders() Forwarders {
return make(map[string]Forwarder) return make(map[string]Forwarder)
} }
// BOZO!! Review!!!
// IsPodForwarded checks if pod has a forward. // IsPodForwarded checks if pod has a forward.
func (ff Forwarders) IsPodForwarded(path string) bool { func (ff Forwarders) IsPodForwarded(path string) bool {
for k := range ff { for k := range ff {
fqn := strings.Split(k, ":") fqn := strings.Split(k, "|")
if fqn[0] == path { if fqn[0] == path {
return true return true
} }
@ -78,18 +82,14 @@ func (ff Forwarders) DeleteAll() {
// Kill stops and delete a port-forwards associated with pod. // Kill stops and delete a port-forwards associated with pod.
func (ff Forwarders) Kill(path string) int { func (ff Forwarders) Kill(path string) int {
hasContainer := strings.Contains(path, ":")
var stats int var stats int
for k, f := range ff { for k, f := range ff {
victim := k victim := k
if !hasContainer {
victim = strings.Split(k, ":")[0]
}
if victim == path { if victim == path {
stats++ stats++
log.Debug().Msgf("Stop + Delete port-forward %s", k) log.Debug().Msgf("Stop + Delete port-forward %s", victim)
f.Stop() f.Stop()
delete(ff, k) delete(ff, victim)
} }
} }

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"testing" "testing"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -238,7 +238,7 @@ func makeDoubleCMKeysContainer(n string, optional bool) *v1.Container {
} }
func load(t *testing.T, n string) *unstructured.Unstructured { func load(t *testing.T, n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err) assert.Nil(t, err)
var o unstructured.Unstructured var o unstructured.Unstructured

Some files were not shown because too many files have changed in this diff Show More