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 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 |
|-----------------|-----------------------------|-----------------------|

View File

@ -14,7 +14,7 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv
## ♫ 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)
* [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`!
* [Andrew Regan](https://github.com/poblish)
* [Astraea](https://github.com/s22s)
* [DataRoots](https://github.com/datarootsio)
* [Bruno Brito](https://github.com/brunohbrito)
* [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.
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!!
---
## 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
# Pod fred
@ -47,9 +58,10 @@ kind: Pod
metadata:
name: fred
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...
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:
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
* [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 #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 #1243](https://github.com/derailed/k9s/issues/1243) Port forward fails in kind on windows 10
---
## 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 #1264](https://github.com/derailed/k9s/pull/1205) Adding note on popeye config
* [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.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")
}
conn, err := client.InitConnection(k8sCfg)
k9sCfg.SetConnection(conn)
if err != nil {
log.Error().Err(err).Msgf("failed to connect to cluster")
} else {
// Try to access server version if that fail. Connectivity issue?
if !k9sCfg.GetConnection().CheckConnectivity() {
log.Panic().Msgf("K9s can't connect to cluster")
}
if !k9sCfg.GetConnection().ConnectionOK() {
panic("No connectivity")
}
log.Info().Msg("✅ Kubernetes connectivity")
if err := k9sCfg.Save(); err != nil {
log.Error().Err(err).Msg("Config save")
}
return k9sCfg
}
// Try to access server version if that fail. Connectivity issue?
if !k9sCfg.GetConnection().CheckConnectivity() {
log.Panic().Msgf("Cannot connect to cluster")
}
if !k9sCfg.GetConnection().ConnectionOK() {
panic("No connectivity")
}
log.Info().Msg("✅ Kubernetes connectivity")
if err := k9sCfg.Save(); err != nil {
log.Error().Err(err).Msg("Config save")
}
return k9sCfg

6
go.mod
View File

@ -14,7 +14,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v4 v4.1.1
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/fsnotify/fsnotify v1.5.1
github.com/fvbommel/sortorder v1.0.2
@ -22,10 +22,6 @@ require (
github.com/ghodss/yaml v1.0.0
github.com/kylelemons/godebug v1.1.0 // indirect
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/rakyll/hey v0.1.4
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/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/tview v0.6.1 h1:dB+9bO7r6a1Yg1HE+XNJj61hioauJnGBFq2biC5bjAk=
github.com/derailed/tview v0.6.1/go.mod h1:5Wjopun0Jw3zxOFtafwc/GlrkFJix1hZz1oQetWpnwE=
github.com/derailed/tview v0.6.3 h1:4GFzcmuVjHYHKlLEpU8lSiUBVfHeYQEC0z5tlBLp4CI=
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 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=
@ -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-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-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-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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()
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) {
return &a, nil
@ -120,11 +120,11 @@ func (a *APIClient) IsActiveNamespace(ns string) bool {
// ActiveNamespace returns the current namespace.
func (a *APIClient) ActiveNamespace() string {
ns, err := a.CurrentNamespaceName()
if err != nil {
return AllNamespaces
if ns, err := a.CurrentNamespaceName(); err == nil {
return ns
}
return ns
return AllNamespaces
}
func (a *APIClient) clearCache() {
@ -261,7 +261,7 @@ func (a *APIClient) CheckConnectivity() bool {
a.reset()
}
} 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
}
@ -301,11 +301,7 @@ func (a *APIClient) Dial() (kubernetes.Interface, error) {
// RestConfig returns a rest api client.
func (a *APIClient) RestConfig() (*restclient.Config, error) {
cfg, err := a.config.RESTConfig()
if err != nil {
return nil, err
}
return cfg, nil
return a.config.RESTConfig()
}
// CachedDiscovery returns a cached discovery client.
@ -430,7 +426,6 @@ func (a *APIClient) supportsMetricsResources() error {
}
apiGroups, err := dial.ServerGroups()
if err != nil {
log.Warn().Err(err).Msgf("Unable to fetch APIGroups")
return err
}
for _, grp := range apiGroups.Groups {

View File

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

View File

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

View File

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

View File

@ -7,33 +7,13 @@ clusters:
- cluster:
insecure-skip-tls-verify: true
server: https://localhost:3002
name: duh
- cluster:
insecure-skip-tls-verify: true
server: https://localhost:3000
name: fred
contexts:
- context:
cluster: blee
user: blee
name: blee
- context:
cluster: duh
user: duh
name: duh
current-context: fred
current-context: blee
kind: Config
preferences: {}
users:
- 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==
users: null

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

View File

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

View File

@ -3,8 +3,8 @@ package config
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"github.com/adrg/xdg"
@ -60,6 +60,15 @@ func K9sHome() string {
if env := os.Getenv(K9sConfig); 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")
if err != nil {
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.
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags) error {
cfg, err := flags.ToRawKubeConfigLoader().RawConfig()
if err != nil {
return err
}
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error {
if isSet(flags.Context) {
c.K9s.CurrentContext = *flags.Context
} 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)
if c.K9s.CurrentContext == "" {
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 {
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.
func (c *Config) Load(path string) error {
f, err := ioutil.ReadFile(path)
f, err := os.ReadFile(path)
if err != nil {
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)
return err
}
return ioutil.WriteFile(path, cfg, 0644)
return os.WriteFile(path, cfg, 0644)
}
// Validate the configuration.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ package dao
import (
"context"
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -41,7 +40,7 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error
}
path, _ := ctx.Value(internal.KeyPath).(string)
ff, err := ioutil.ReadDir(dir)
ff, err := os.ReadDir(dir)
if err != nil {
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)) {
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

View File

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

View File

@ -3,7 +3,7 @@ package dao
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
@ -29,7 +29,7 @@ func TestCruiserSlice(t *testing.T) {
// Helpers...
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)
var o unstructured.Unstructured

View File

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

View File

@ -69,20 +69,20 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
return err
}
ns, _ := client.Namespaced(path)
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", []string{client.PatchVerb})
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to restart a deployment")
}
update, err := polymorphichelpers.ObjectRestarterFn(dp)
dial, err := d.Client().Dial()
if err != nil {
return err
}
dial, err := d.Client().Dial()
restarter, err := polymorphichelpers.ObjectRestarterFn(dp)
if err != nil {
return err
}
@ -91,7 +91,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
ctx,
dp.Name,
types.StrategicMergePatchType,
update,
restarter,
metav1.PatchOptions{},
)
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)
}
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)
if !ok {
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.Init(f, client.NewGVR("v1/pods"))
for _, o := range oo {
var pod v1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
if err != nil {
return err
u, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected unstructured got %t", o)
}
opts = opts.Clone()
opts.Path = client.FQN(pod.Namespace, pod.Name)
if err := po.TailLogs(ctx, c, opts); err != nil {
opts.Path = client.FQN(u.GetNamespace(), u.GetName())
if err := po.TailLogs(ctx, out, opts); err != nil {
return err
}
}

View File

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

View File

@ -2,39 +2,31 @@ package dao
import (
"bytes"
"fmt"
"regexp"
"time"
"github.com/derailed/k9s/internal/color"
)
// LogChan represents a channel for logs.
type LogChan chan *LogItem
var ItemEOF = new(LogItem)
// LogItem represents a container log line.
type LogItem struct {
Pod, Container, Timestamp string
SingleContainer bool
Bytes []byte
Pod, Container string
SingleContainer bool
Bytes []byte
}
// NewLogItem returns a new item.
func NewLogItem(b []byte) *LogItem {
space := []byte(" ")
cols := bytes.Split(b[:len(b)-1], space)
func NewLogItem(bb []byte) *LogItem {
return &LogItem{
Timestamp: string(cols[0]),
Bytes: bytes.Join(cols[1:], space),
Bytes: bb,
}
}
// NewLogItemFromString returns a new item.
func NewLogItemFromString(s string) *LogItem {
return &LogItem{
Bytes: []byte(s),
Timestamp: time.Now().String(),
Bytes: []byte(s),
}
}
@ -46,22 +38,18 @@ func (l *LogItem) ID() string {
return l.Container
}
// Clone copies an item.
func (l *LogItem) Clone() *LogItem {
bytes := make([]byte, len(l.Bytes))
copy(bytes, l.Bytes)
return &LogItem{
Container: l.Container,
Pod: l.Pod,
Timestamp: l.Timestamp,
SingleContainer: l.SingleContainer,
Bytes: bytes,
// GetTimestamp fetch log lime timestamp
func (l *LogItem) GetTimestamp() string {
index := bytes.Index(l.Bytes, []byte{' '})
if index < 0 {
return ""
}
return string(l.Bytes[:index])
}
// Info returns pod and container information.
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.
@ -69,37 +57,39 @@ func (l *LogItem) IsEmpty() bool {
return len(l.Bytes) == 0
}
var (
escPattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
matcher = []byte("$1[]")
)
// Size returns the size of the item.
func (l *LogItem) Size() int {
return 100 + len(l.Bytes) + len(l.Pod) + len(l.Container)
}
// Render returns a log line as string.
func (l *LogItem) Render(paint int, showTime bool) []byte {
bb := make([]byte, 0, 200)
if showTime {
t := l.Timestamp
for i := len(t); i < 30; i++ {
t += " "
func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) {
index := bytes.Index(l.Bytes, []byte{' '})
if showTime && index > 0 {
bb.WriteString("[gray::]")
bb.Write(l.Bytes[:index])
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 != "" {
bb = append(bb, color.ANSIColorize(l.Pod, paint)...)
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, ' ')
bb.WriteString("[" + paint + "::]" + l.Pod)
}
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
import (
"bytes"
"fmt"
"testing"
@ -34,13 +35,13 @@ func TestLogItemRender(t *testing.T) {
}{
"empty": {
opts: dao.LogOptions{},
e: "Testing 1,2,3...",
e: "Testing 1,2,3...\n",
},
"container": {
opts: dao.LogOptions{
Container: "fred",
},
e: "\x1b[38;5;0mfred\x1b[0m Testing 1,2,3...",
e: "[yellow::b]fred[-::-] Testing 1,2,3...\n",
},
"pod": {
opts: dao.LogOptions{
@ -48,7 +49,7 @@ func TestLogItemRender(t *testing.T) {
Container: "blee",
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": {
opts: dao.LogOptions{
@ -57,7 +58,7 @@ func TestLogItemRender(t *testing.T) {
SingleContainer: 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)
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..."))
i := dao.NewLogItem(s)
i.Pod, i.Container = "fred", "blee"
b.ResetTimer()
b.ReportAllocs()
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
import (
"bytes"
"fmt"
"regexp"
"strings"
"sync"
"github.com/gdamore/tcell/v2"
"github.com/sahilm/fuzzy"
)
var colorPalette = []tcell.Color{
tcell.ColorTeal,
tcell.ColorGreen,
tcell.ColorPurple,
tcell.ColorLime,
tcell.ColorBlue,
tcell.ColorYellow,
tcell.ColorFuchsia,
tcell.ColorAqua,
var podPalette = []string{
"teal",
"green",
"purple",
"lime",
"blue",
"yellow",
"fushia",
"aqua",
}
// LogItems represents a collection of log items.
type LogItems struct {
items []*LogItem
colors map[string]tcell.Color
mx sync.RWMutex
items []*LogItem
podColors map[string]string
mx sync.RWMutex
}
// NewLogItems returns a new instance.
func NewLogItems() *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()
defer l.mx.Unlock()
l.items = nil
for k := range l.colors {
delete(l.colors, k)
l.items = l.items[:0]
for k := range l.podColors {
delete(l.podColors, k)
}
}
@ -76,8 +76,8 @@ func (l *LogItems) Subset(index int) *LogItems {
defer l.mx.RUnlock()
return &LogItems{
items: l.items[index:],
colors: l.colors,
items: l.items[index:],
podColors: l.podColors,
}
}
@ -87,8 +87,8 @@ func (l *LogItems) Merge(n *LogItems) {
defer l.mx.Unlock()
l.items = append(l.items, n.items...)
for k, v := range n.colors {
l.colors[k] = v
for k, v := range n.podColors {
l.podColors[k] = v
}
}
@ -101,47 +101,60 @@ func (l *LogItems) Add(ii ...*LogItem) {
}
// 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()
defer l.mx.Unlock()
ll := make([][]byte, len(l.items))
for i, item := range l.items {
color := l.colors[item.ID()]
ll[i] = item.Render(int(color-tcell.ColorValid), showTime)
var colorIndex int
for i, item := range l.items[index:] {
id := item.ID()
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.
func (l *LogItems) StrLines(showTime bool) []string {
func (l *LogItems) StrLines(index int, showTime bool) []string {
l.mx.Lock()
defer l.mx.Unlock()
ll := make([]string, len(l.items))
for i, item := range l.items {
ll[i] = string(item.Render(0, showTime))
ll := make([]string, len(l.items[index:]))
for i, item := range l.items[index:] {
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
item.Render("white", showTime, bb)
ll[i] = bb.String()
}
return ll
}
// Render returns logs as a collection of strings.
func (l *LogItems) Render(showTime bool, ll [][]byte) {
index := len(l.colors)
for i, item := range l.items {
func (l *LogItems) Render(index int, showTime bool, ll [][]byte) {
var colorIndex int
for i, item := range l.items[index:] {
id := item.ID()
color, ok := l.colors[id]
color, ok := l.podColors[id]
if !ok {
if index >= len(colorPalette) {
index = 0
if colorIndex >= len(podPalette) {
colorIndex = 0
}
color = colorPalette[index]
l.colors[id] = color
index++
color = podPalette[colorIndex]
l.podColors[id] = color
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.
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 == "" {
return nil, nil, nil
}
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
}
matches, indices, err := l.filterLogs(q, showTime)
matches, indices, err := l.filterLogs(index, q, showTime)
if err != nil {
return nil, nil, err
}
@ -170,10 +183,10 @@ func (l *LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) {
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)
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 {
matches = append(matches, m.Index)
indices = append(indices, m.MatchedIndexes)
@ -182,7 +195,7 @@ func (l *LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
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
if IsInverseSelector(q) {
invert = true
@ -193,7 +206,9 @@ func (l *LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
return nil, nil, err
}
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)
if locs != nil && invert {
continue

View File

@ -71,7 +71,7 @@ func TestLogItemsFilter(t *testing.T) {
for _, i := range ii.Items() {
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)
if err == nil {
assert.Equal(t, u.e, res)
@ -87,20 +87,20 @@ func TestLogItemsRender(t *testing.T) {
}{
"empty": {
opts: dao.LogOptions{},
e: "Testing 1,2,3...",
e: "Testing 1,2,3...\n",
},
"container": {
opts: dao.LogOptions{
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{
Path: "blee/fred",
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": {
opts: dao.LogOptions{
@ -108,7 +108,7 @@ func TestLogItemsRender(t *testing.T) {
Container: "blee",
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
t.Run(k, func(t *testing.T) {
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]))
})
}

View File

@ -2,7 +2,6 @@ package dao
import (
"fmt"
"strings"
"time"
"github.com/derailed/k9s/internal/client"
@ -12,12 +11,14 @@ import (
// LogOptions represents logger options.
type LogOptions struct {
CreateDuration time.Duration
Path string
Container string
DefaultContainer string
SinceTime string
Lines int64
SinceSeconds int64
Head bool
Previous bool
SingleContainer bool
MultiPods bool
@ -77,6 +78,18 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
Previous: o.Previous,
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 {
return &opts
}
@ -96,21 +109,6 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
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.
func (o *LogOptions) DecorateLog(bytes []byte) *LogItem {
item := NewLogItem(bytes)

View File

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

View File

@ -177,7 +177,7 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
}
// 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)
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
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 {
opts.DefaultContainer = co
return tailLogs(ctx, p, c, opts)
return tailLogs(ctx, p, out, opts)
}
if opts.HasContainer() && !opts.AllContainers {
return tailLogs(ctx, p, c, opts)
return tailLogs(ctx, p, out, opts)
}
var tailed bool
for _, co := range po.Spec.InitContainers {
o := opts.Clone()
o.Container = co.Name
if err := tailLogs(ctx, p, c, o); err != nil {
if err := tailLogs(ctx, p, out, o); err != nil {
return err
}
tailed = true
@ -217,7 +217,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
for _, co := range po.Spec.Containers {
o := opts.Clone()
o.Container = co.Name
if err := tailLogs(ctx, p, c, o); err != nil {
if err := tailLogs(ctx, p, out, o); err != nil {
return err
}
tailed = true
@ -225,7 +225,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
for _, co := range po.Spec.EphemeralContainers {
o := opts.Clone()
o.Container = co.Name
if err := tailLogs(ctx, p, c, o); err != nil {
if err := tailLogs(ctx, p, out, o); err != nil {
return err
}
tailed = true
@ -326,21 +326,22 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error
// ----------------------------------------------------------------------------
// Helpers...
func tailLogs(ctx context.Context, logger Logger, c LogChan, opts *LogOptions) error {
log.Debug().Msgf("Tailing logs for %#v", opts)
func tailLogs(ctx context.Context, logger Logger, out LogChan, opts *LogOptions) error {
var (
err error
req *restclient.Request
stream io.ReadCloser
)
o := opts.ToPodLogOptions()
log.Debug().Msgf("TAIL_LOGS! %#v", o)
done:
for r := 0; r < logRetryCount; r++ {
req, err = logger.Logs(opts.Path, opts.ToPodLogOptions())
req, err = logger.Logs(opts.Path, o)
if err == nil {
// This call will block if nothing is in the stream!!
if stream, err = req.Stream(ctx); err == nil {
go readLogs(stream, c, opts)
go readLogs(ctx, stream, out, opts)
break
} else {
log.Error().Err(err).Msg("Streaming logs")
@ -351,6 +352,7 @@ done:
select {
case <-ctx.Done():
log.Debug().Msgf("!!!!TAIL_LOGS CANCELED!!!!")
err = ctx.Err()
break done
default:
@ -358,36 +360,36 @@ done:
}
}
if err != nil {
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
return err
}
func readLogs(stream io.ReadCloser, c LogChan, opts *LogOptions) {
func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOptions) {
defer func() {
log.Debug().Msgf(">>> Closing stream %s", opts.Info())
log.Debug().Msgf("READ_LOGS BAILED!!!")
if err := stream.Close(); err != nil {
log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info())
}
}()
log.Debug().Msgf("READ_LOGS PROCESSING %#v", opts)
r := bufio.NewReader(stream)
for {
bytes, err := r.ReadBytes('\n')
if err != nil {
if errors.Is(err, io.EOF) {
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info())
// c <- ItemEOF
return
}
log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info())
c <- opts.DecorateLog([]byte(fmt.Sprintf("\nlog stream failed: %#v\n", err)))
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"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/port"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -28,8 +29,7 @@ type PortForwarder struct {
stopChan, readyChan chan struct{}
active bool
path string
container string
ports []string
tunnel port.PortTunnel
age time.Time
}
@ -57,58 +57,56 @@ func (p *PortForwarder) SetActive(b bool) {
p.active = b
}
// Ports returns the forwarded ports mappings.
func (p *PortForwarder) Ports() []string {
return p.ports
// Port returns the port mapping.
func (p *PortForwarder) Port() string {
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.
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.
func PortForwardID(path, co string) string {
return path + ":" + co
// ID returns a pf id.
func (p *PortForwarder) ID() string {
return PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap())
}
// Container returns the target's container.
func (p *PortForwarder) Container() string {
return p.container
return p.tunnel.Container
}
// Stop terminates a port forward.
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
close(p.stopChan)
}
// FQN returns the portforward unique id.
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.
func (p *PortForwarder) HasPortMapping(m string) bool {
for _, mapping := range p.ports {
if mapping == m {
return true
}
}
return false
func (p *PortForwarder) HasPortMapping(portMap string) bool {
return p.tunnel.PortMap() == portMap
}
// 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) {
if len(tt) == 0 {
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()
func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.PortForwarder, error) {
p.path, p.tunnel, p.age = path, tt, time.Now()
ns, n := client.Namespaced(path)
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).
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()
if err != nil {
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)
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...
// PortForwardID computes port-forward identifier.
func PortForwardID(path, co, portMap string) string {
return path + "|" + co + "|" + portMap
}
func codec() (serializer.CodecFactory, runtime.ParameterCodec) {
scheme := runtime.NewScheme()
gv := schema.GroupVersion{Group: "", Version: "v1"}

View File

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

View File

@ -3,7 +3,7 @@ package dao
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
@ -89,7 +89,7 @@ func TestExtractString(t *testing.T) {
// Helpers...
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)
var o unstructured.Unstructured

View File

@ -3,7 +3,6 @@ package dao
import (
"context"
"errors"
"io/ioutil"
"os"
"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")
}
ff, err := ioutil.ReadDir(SanitizeFilename(dir))
ff, err := os.ReadDir(SanitizeFilename(dir))
if err != nil {
return nil, err
}
oo := make([]runtime.Object, len(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

View File

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

View File

@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"io"
"net/http"
"time"
@ -55,13 +55,7 @@ func NewClusterMeta() ClusterMeta {
// Deltas diffs cluster meta return true if different, false otherwise.
func (c ClusterMeta) Deltas(n ClusterMeta) bool {
if c.Cpu != n.Cpu {
return true
}
if c.Mem != n.Mem {
return true
}
if c.Ephemeral != n.Ephemeral {
if c.Cpu != n.Cpu || c.Mem != n.Mem || c.Ephemeral != n.Ephemeral {
return true
}
@ -76,6 +70,7 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool {
// ClusterInfo models cluster metadata.
type ClusterInfo struct {
cluster *Cluster
factory dao.Factory
data ClusterMeta
version string
listeners []ClusterInfoListener
@ -83,11 +78,12 @@ type ClusterInfo struct {
}
// NewClusterInfo returns a new instance.
func NewClusterInfo(f dao.Factory, version string) *ClusterInfo {
func NewClusterInfo(f dao.Factory, v string) *ClusterInfo {
c := ClusterInfo{
factory: f,
cluster: NewCluster(f),
data: NewClusterMeta(),
version: version,
version: v,
cache: cache.NewLRUExpireCache(cacheSize),
}
@ -116,28 +112,29 @@ func (c *ClusterInfo) Reset(f dao.Factory) {
c.Refresh()
}
// Refresh fetches latest cluster meta.
// Refresh fetches the latest cluster meta.
func (c *ClusterInfo) Refresh() {
data := NewClusterMeta()
data.Context = c.cluster.ContextName()
data.Cluster = c.cluster.ClusterName()
data.User = c.cluster.UserName()
if c.factory.Client().ConnectionOK() {
data.Context = c.cluster.ContextName()
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
v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev())
data.K9sVer, data.K9sLatest = v1.String(), v2.String()
if v1.IsCurrent(v2) {
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) {
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 {
return "", err
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package model_test
import (
"context"
"fmt"
"strconv"
"testing"
@ -32,9 +33,8 @@ func TestLogFullBuffer(t *testing.T) {
m.Notify()
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, data.Items()[4:].Lines(false), v.data)
}
func TestLogFilter(t *testing.T) {
@ -79,13 +79,13 @@ func TestLogFilter(t *testing.T) {
m.Notify()
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, u.e, len(v.data))
m.ClearFilter()
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, size, len(v.data))
})
@ -99,7 +99,10 @@ func TestLogStartStop(t *testing.T) {
v := newTestView()
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.Add(dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2"))
for _, d := range data.Items() {
@ -109,7 +112,7 @@ func TestLogStartStop(t *testing.T) {
m.Stop()
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, 2, len(v.data))
}
@ -132,7 +135,7 @@ func TestLogClear(t *testing.T) {
m.Clear()
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, len(v.data))
}
@ -151,7 +154,9 @@ func TestLogBasic(t *testing.T) {
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled)
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) {
@ -163,7 +168,9 @@ func TestLogAppend(t *testing.T) {
items := dao.NewLogItems()
items.Add(dao.NewLogItemFromString("blah blah"))
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.Add(
@ -174,7 +181,9 @@ func TestLogAppend(t *testing.T) {
m.Append(d)
}
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()
assert.Equal(t, 2, v.dataCalled)
@ -203,7 +212,7 @@ func TestLogTimedout(t *testing.T) {
}
m.Notify()
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 1, v.clearCalled)
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"
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.Init(makeFactory())
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())
m.ToggleAllContainers()
m.ToggleAllContainers(ctx, c)
assert.Equal(t, "blee", m.GetContainer())
}
@ -245,16 +258,17 @@ func newTestView() *testView {
return &testView{}
}
func (t *testView) LogCanceled() {}
func (t *testView) LogStop() {}
func (t *testView) LogResume() {}
func (t *testView) LogChanged(ll [][]byte) {
t.data = ll
t.dataCalled++
}
func (t *testView) LogCleared() {
t.clearCalled++
t.data = nil
}
func (t *testView) LogFailed(err error) {
fmt.Println("LogErr", err)
t.errCalled++

View File

@ -164,7 +164,7 @@ func (t *Table) Peek() render.TableData {
}
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.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval

View File

@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/derailed/k9s/internal"
@ -139,7 +139,7 @@ func TestTableGenericHydrate(t *testing.T) {
// Helpers...
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 {
panic(err)
}
@ -151,7 +151,7 @@ func mustLoad(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)
var o unstructured.Unstructured
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 {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err)
return raw
}

View File

@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/derailed/k9s/internal"
@ -122,7 +122,7 @@ func makeTableFactory() tableFactory {
}
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 {
panic(err)
}

View File

@ -5,7 +5,6 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"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 {
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 (
"errors"
"fmt"
"io/ioutil"
"os"
"regexp"
"strconv"
@ -97,7 +96,7 @@ func (Benchmark) diagnose(ns string, ff Fields) error {
// Helpers...
func (Benchmark) readFile(file string) (string, error) {
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
return "", err
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ package render_test
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
@ -13,7 +13,7 @@ import (
// Helpers...
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)
var o unstructured.Unstructured

View File

@ -41,7 +41,7 @@ func NewApp(cfg *config.Config, context string) *App {
a.views = map[string]tview.Primitive{
"menu": NewMenu(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),
}
@ -60,6 +60,9 @@ func (a *App) Init() {
// QueueUpdate queues up a ui action.
func (a *App) QueueUpdate(f func()) {
if a.Application == nil {
return
}
go func() {
a.Application.QueueUpdate(f)
}()
@ -67,6 +70,9 @@ func (a *App) QueueUpdate(f func()) {
// QueueUpdateDraw queues up a ui action and redraw the ui.
func (a *App) QueueUpdateDraw(f func()) {
if a.Application == nil {
return
}
go func() {
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")
return
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 {
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")
return
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 {
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()).
SetLabelColor(styles.LabelFgColor.Color()).
SetFieldTextColor(styles.FieldFgColor.Color())
f.AddCheckbox("Cascade:", cascade, func(checked bool) {
f.AddCheckbox("Cascade:", cascade, func(_ string, checked bool) {
cascade = checked
})
f.AddCheckbox("Force:", force, func(checked bool) {
f.AddCheckbox("Force:", force, func(_ string, checked bool) {
force = checked
})
f.AddButton("Cancel", func() {

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import (
"fmt"
"os"
"os/signal"
"runtime"
"sort"
"strings"
"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.AddListener(a.clusterInfo())
a.clusterModel.AddListener(a.statusIndicator())
a.clusterModel.Refresh()
a.clusterInfo().Init()
if a.Conn().ConnectionOK() {
a.clusterModel.Refresh()
a.clusterInfo().Init()
}
a.command = NewCommand(a)
if err := a.command.Init(); err != nil {
@ -185,6 +188,7 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
func (a *App) bindKeys() {
a.AddActions(ui.KeyActions{
ui.KeyShiftG: ui.NewSharedKeyAction("DumpGOR", a.dumpGOR, false),
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, 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.
func (a *App) ActiveView() 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.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 (
"context"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -71,7 +71,7 @@ func benchDir(cfg *config.Config) string {
}
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 {
return "", err
}

View File

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

View File

@ -2,6 +2,7 @@ package view
import (
"fmt"
"runtime"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
@ -102,7 +103,7 @@ func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
c.app.QueueUpdateDraw(func() {
c.Clear()
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.User)
if curr.K9sLatest != "" {

View File

@ -4,11 +4,11 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/port"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell/v2"
@ -178,89 +178,65 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
ports, ok := c.isForwardable(path)
ports, ann, ok := c.listForwardable(path)
if !ok {
return nil
}
ShowPortForwards(c, c.GetTable().Path, ports, "", startFwdCB)
ShowPortForwards(c, c.GetTable().Path, ports, ann, startFwdCB)
return nil
}
func (c *Container) isForwardable(path string) ([]string, bool) {
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
}
func checkRunningStatus(co string, ss []v1.ContainerStatus) error {
var cs *v1.ContainerStatus
ss := po.Status.ContainerStatuses
for i := range ss {
if ss[i].Name == path {
if ss[i].Name == co {
cs = &ss[i]
break
}
}
if cs == nil {
log.Error().Err(fmt.Errorf("unable to locate container status for %q", path))
return nil, false
return fmt.Errorf("unable to locate container status for %q", co)
}
if render.ToContainerState(cs.State) != "Running" {
c.App().Flash().Err(fmt.Errorf("Container %s is not running?", path))
return nil, false
return fmt.Errorf("Container %s is not running?", co)
}
portC := render.ToContainerPorts(co.Ports)
ports := strings.Split(portC, ",")
if len(ports) == 0 {
return nil
}
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"))
return nil, false
return nil, nil, false
}
pp := make([]string, 0, len(ports))
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
return port.FromContainerPorts(path, co.Ports), po.Annotations, true
}

View File

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

View File

@ -45,13 +45,13 @@ func ShowDrain(view ResourceViewer, path string, defaults dao.DrainOptions, okFn
view.App().Flash().Clear()
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
})
f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(v bool) {
f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(_ string, v bool) {
opts.DeleteEmptyDirData = v
})
f.AddCheckbox("Force:", defaults.Force, func(v bool) {
f.AddCheckbox("Force:", defaults.Force, func(_ string, v bool) {
opts.Force = v
})

View File

@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
@ -32,6 +33,7 @@ const (
type shellOpts struct {
clear, background bool
pipes []string
binary string
banner string
args []string
@ -96,40 +98,41 @@ func execute(opts shellOpts) error {
}
ctx, cancel := context.WithCancel(context.Background())
defer func() {
cancel()
clearScreen()
if !opts.background {
cancel()
clearScreen()
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
go func(cancel context.CancelFunc) {
defer log.Debug().Msgf("SIGNAL_GOR - BAILED!!")
select {
case <-sigChan:
log.Debug().Msg("Command canceled with signal!")
log.Debug().Msgf("Command canceled with signal!")
cancel()
case <-ctx.Done():
return
log.Debug().Msgf("SIGNAL Context CANCELED!")
}
}()
}(cancel)
log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " "))
cmd := exec.Command(opts.binary, opts.args...)
cmds := make([]*exec.Cmd, 0, 1)
cmd := exec.CommandContext(ctx, opts.binary, opts.args...)
log.Debug().Msgf("RUNNING> %s", cmd)
cmds = append(cmds, cmd)
var err error
if opts.background {
err = cmd.Start()
} else {
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
_, _ = cmd.Stdout.Write([]byte(opts.banner))
err = cmd.Run()
for _, p := range opts.pipes {
tokens := strings.Split(p, " ")
if len(tokens) < 2 {
continue
}
cmd := exec.CommandContext(ctx, tokens[0], tokens[1:]...)
log.Debug().Msgf("\t| %s", cmd)
cmds = append(cmds, cmd)
}
select {
case <-ctx.Done():
return errors.New("canceled by operator")
default:
return err
}
return pipe(ctx, opts, cmds...)
}
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.
func asKey(key string) (tcell.Key, error) {
for k, v := range tcell.KeyNames {
if v == key {
if key == v {
return k, nil
}
}

View File

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

View File

@ -1,14 +1,13 @@
package view
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/atotto/clipboard"
@ -25,7 +24,7 @@ import (
const (
logTitle = "logs"
logMessage = "Waiting for logs..."
logMessage = "Waiting for logs...\n"
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:-]] "
flushTimeout = 1 * time.Millisecond
@ -35,11 +34,16 @@ const (
type Log struct {
*tview.Flex
app *App
logs *Logger
indicator *LogIndicator
ansiWriter io.Writer
model *model.Log
app *App
logs *Logger
indicator *LogIndicator
ansiWriter io.Writer
model *model.Log
cancelFn context.CancelFunc
cancelUpdates bool
mx sync.Mutex
logChan dao.LogChan
follow bool
}
var _ model.Component = (*Log)(nil)
@ -47,8 +51,10 @@ var _ model.Component = (*Log)(nil)
// NewLog returns a new viewer.
func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log {
l := Log{
Flex: tview.NewFlex(),
model: model.NewLog(gvr, opts, flushTimeout),
Flex: tview.NewFlex(),
logChan: make(dao.LogChan, 2),
model: model.NewLog(gvr, opts, flushTimeout),
follow: true,
}
return &l
@ -76,21 +82,18 @@ func (l *Log) Init(ctx context.Context) (err error) {
return err
}
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.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize)
l.logs.cmdBuff.AddListener(l)
l.logs.SetMaxLines(l.app.Config.K9s.Logger.BufferSize)
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.bindKeys()
l.StylesChanged(l.app.Styles)
l.app.Styles.AddListener(l)
l.goFullScreen()
l.model.Init(l.app.factory)
l.model.AddListener(l)
l.updateTitle()
l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime)
@ -103,6 +106,30 @@ func (l *Log) InCmdMode() bool {
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.
func (l *Log) LogCleared() {
l.app.QueueUpdateDraw(func() {
@ -126,6 +153,9 @@ func (l *Log) LogFailed(err error) {
// LogChanged updates the logs.
func (l *Log) LogChanged(lines [][]byte) {
l.app.QueueUpdateDraw(func() {
if l.logs.GetText(true) == logMessage {
l.logs.Clear()
}
l.Flush(lines)
})
}
@ -166,15 +196,43 @@ func (l *Log) ExtraHints() map[string]string {
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.
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.
func (l *Log) Stop() {
l.model.Stop()
log.Debug().Msgf("LOG_VIEW STOPPED!")
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.logs.cmdBuff.RemoveListener(l)
l.logs.cmdBuff.RemoveListener(l.app.Prompt())
@ -185,12 +243,13 @@ func (l *Log) Name() string { return logTitle }
func (l *Log) bindKeys() {
l.logs.Actions().Set(ui.KeyActions{
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
ui.Key0: ui.NewKeyAction("tail", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("head", l.head(), true),
ui.Key2: ui.NewKeyAction("1m", l.sinceCmd(60), true),
ui.Key3: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
ui.Key4: ui.NewKeyAction("15m", l.sinceCmd(15*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.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true),
@ -242,13 +301,17 @@ func (l *Log) Indicator() *LogIndicator {
}
func (l *Log) updateTitle() {
sinceSeconds, since := l.model.SinceSeconds(), "all"
sinceSeconds, since := l.model.SinceSeconds(), "tail"
if sinceSeconds > 0 && sinceSeconds < 60*60 {
since = fmt.Sprintf("%dm", sinceSeconds/60)
}
if sinceSeconds >= 60*60 {
since = fmt.Sprintf("%dh", sinceSeconds/(60*60))
}
if l.model.IsHead() {
since = "head"
}
var title string
path, co := l.model.GetPath(), l.model.GetContainer()
if co == "" {
@ -274,25 +337,47 @@ var EOL = []byte{'\n'}
// Flush write logs to viewer.
func (l *Log) Flush(lines [][]byte) {
log.Debug().Msgf("LINES [%d]%d", runtime.NumGoroutine(), len(strings.Split(l.logs.GetText(true), "\n")))
if !l.indicator.AutoScroll() {
defer func() {
if l.cancelUpdates {
l.cancelUpdates = false
}
}()
if len(lines) == 0 || !l.indicator.AutoScroll() || l.cancelUpdates {
return
}
_, _ = l.ansiWriter.Write(EOL)
if _, err := l.ansiWriter.Write(bytes.Join(lines, EOL)); err != nil {
log.Error().Err(err).Msgf("write logs failed")
for i := 0; i < len(lines); i++ {
if l.cancelUpdates {
break
}
_, _ = l.ansiWriter.Write(lines[i])
}
if l.follow {
l.logs.ScrollToEnd()
}
l.logs.ScrollToEnd()
l.indicator.Refresh()
}
// ----------------------------------------------------------------------------
// 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 {
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()
return nil
}
}
@ -302,7 +387,7 @@ func (l *Log) toggleAllContainers(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
l.indicator.ToggleAllContainers()
l.model.ToggleAllContainers()
l.model.ToggleAllContainers(l.getContext(), l.logChan)
l.updateTitle()
return nil
@ -322,7 +407,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
// SaveCmd dumps the logs to file.
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)
} else {
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 {
_, _, 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
}
@ -388,6 +475,7 @@ func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
l.indicator.ToggleTimestamp()
l.model.ToggleShowTimestamp(l.indicator.showTime)
l.indicator.Refresh()
return nil
}
@ -399,6 +487,8 @@ func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey {
l.indicator.ToggleTextWrap()
l.logs.SetWrap(l.indicator.textWrap)
l.indicator.Refresh()
return nil
}
@ -409,11 +499,15 @@ func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
}
l.indicator.ToggleAutoScroll()
if l.indicator.AutoScroll() {
l.model.Start()
} else {
l.model.Stop()
}
l.follow = l.indicator.AutoScroll()
// if l.indicator.AutoScroll() {
// // l.model.Restart(l.getContext(), l.logChan, false)
// } else {
// // l.model.Stop()
// }
l.indicator.Refresh()
return nil
}
@ -423,6 +517,8 @@ func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
}
l.indicator.ToggleFullScreen()
l.goFullScreen()
l.indicator.Refresh()
return nil
}

View File

@ -7,17 +7,7 @@ import (
"github.com/derailed/tview"
)
const (
autoscroll = "Autoscroll"
fullscreen = "FullScreen"
timestamp = "Timestamps"
wrap = "Wrap"
allContainers = "AllContainers"
on = "[limegreen::]On"
off = "[gray::]Off"
spacer = " "
bold = "[-::b]"
)
const spacer = " "
// LogIndicator represents a log view indicator.
type LogIndicator struct {
@ -25,6 +15,7 @@ type LogIndicator struct {
styles *config.Styles
scrollStatus int32
indicator []byte
fullScreen bool
textWrap bool
showTime bool
@ -33,15 +24,16 @@ type LogIndicator struct {
}
// 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{
styles: styles,
TextView: tview.NewTextView(),
indicator: make([]byte, 0, 100),
scrollStatus: 1,
fullScreen: cfg.K9s.Logger.FullScreenLogs,
textWrap: cfg.K9s.Logger.TextWrap,
showTime: cfg.K9s.Logger.ShowTime,
shouldDisplayAllContainers: isContainerLogView,
shouldDisplayAllContainers: allContainers,
}
l.StylesChanged(styles)
styles.AddListener(&l)
@ -110,24 +102,46 @@ func (l *LogIndicator) ToggleAllContainers() {
l.Refresh()
}
// Refresh updates the view.
func (l *LogIndicator) Refresh() {
func (l *LogIndicator) reset() {
l.Clear()
if l.shouldDisplayAllContainers {
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, "")
l.indicator = l.indicator[:0]
}
func (l *LogIndicator) update(title string, state bool, padding string) {
bb := []byte(bold + title + ":")
if state {
bb = append(bb, []byte(on)...)
} else {
bb = append(bb, []byte(off)...)
// Refresh updates the view.
func (l *LogIndicator) Refresh() {
l.reset()
if l.shouldDisplayAllContainers {
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
e string
}{
"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",
"all-containers": {
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": {
view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[-::b]Autoscroll:[limegreen::]On [-::b]FullScreen:[gray::]Off [-::b]Timestamps:[gray::]Off [-::b]Wrap:[gray::]Off\n",
"plain": {
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]
t.Run(k, func(t *testing.T) {
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().Notify()
assert.Equal(t, 15, len(v.Hints()))
assert.Equal(t, 16, len(v.Hints()))
v.toggleAutoScrollCmd(nil)
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{
Pod: "fred/blee",
Container: "c1",
Timestamp: "ttt",
Bytes: []byte("Testing 1, 2, 3"),
Bytes: []byte("ttt Testing 1, 2, 3\n"),
},
)
var list logList
@ -84,9 +83,11 @@ func TestLogTimestamp(t *testing.T) {
l.GetModel().Set(ii)
l.SendKeys(ui.KeyT)
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.clear)
assert.Equal(t, 0, list.fail)
@ -131,5 +132,8 @@ func (l *logList) LogChanged(ll [][]byte) {
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) LogFailed(error) { l.fail++ }

View File

@ -3,7 +3,7 @@ package view_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -25,10 +25,32 @@ func TestLog(t *testing.T) {
v.Init(makeContext())
ii := dao.NewLogItems()
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo"))
v.Flush(ii.Lines(false))
ii.Add(dao.NewLogItemFromString("blee\n"), dao.NewLogItemFromString("bozo\n"))
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) {
@ -41,13 +63,17 @@ func BenchmarkLogFlush(b *testing.B) {
items := dao.NewLogItems()
items.Add(
dao.NewLogItemFromString("blee"),
dao.NewLogItemFromString("bozo"),
dao.NewLogItemFromString("\033[0;30mblee\n"),
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.ResetTimer()
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()
ii := dao.NewLogItems()
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"
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir)
c1, _ := os.ReadDir(dir)
v.SaveCmd(nil)
c2, _ := ioutil.ReadDir(dir)
c2, _ := os.ReadDir(dir)
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) {}
// BufferCompleted indicates input was accepted.
func (l *Logger) BufferCompleted(s string) {
}
func (l *Logger) BufferCompleted(s string) {}
// BufferActive indicates the buff activity changed.
func (l *Logger) BufferActive(state bool, k model.BufferKind) {

View File

@ -3,6 +3,7 @@ package view
import (
"context"
"fmt"
"regexp"
"time"
"github.com/derailed/k9s/internal"
@ -138,19 +139,35 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
path := p.GetTable().GetSelectedItem()
if path == "" {
return nil
selections := p.GetTable().GetSelectedItems()
if len(selections) == 0 {
return evt
}
showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", path), func() {
var pf dao.PortForward
pf.Init(p.App().factory, client.NewGVR("portforwards"))
if err := pf.Delete(path, true, true); err != nil {
p.Stop()
defer p.Start()
var msg string
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)
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()
})
@ -160,6 +177,16 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
// ----------------------------------------------------------------------------
// 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()) {
m := tview.NewModal().
AddButtons([]string{"Cancel", "OK"}).

View File

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

View File

@ -6,44 +6,6 @@ import (
"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) {
uu := map[string]struct {
port, e string

View File

@ -1,13 +1,10 @@
package view
import (
"errors"
"fmt"
"net"
"strconv"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/port"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/watch"
"github.com/gdamore/tcell/v2"
@ -19,8 +16,6 @@ import (
"k8s.io/client-go/tools/portforward"
)
const AnnDefaultPF = "k9s.imhotep.io/default-portforward-container"
// PortForwardExtender adds port-forward extensions.
type PortForwardExtender struct {
ResourceViewer
@ -78,19 +73,11 @@ func (p *PortForwardExtender) fetchPodName(path string) (string, error) {
// ----------------------------------------------------------------------------
// 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) {
v.App().factory.AddForwarder(pf)
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)
})
@ -106,67 +93,77 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward
})
}
func startFwdCB(v ResourceViewer, path, co string, tt []client.PortTunnel) {
for _, t := range tt {
err := tryListenPort(t.Address, t.LocalPort)
if err != nil {
v.App().Flash().Err(err)
return
func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error {
if err := pts.CheckAvailable(); err != nil {
return err
}
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 {
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)
return nil
}
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 {
return err
}
ports := make([]string, 0, len(mm))
ports := make(port.ContainerPortSpecs, 0, len(mm))
for co, pp := range mm {
for _, p := range pp {
if p.Protocol != v1.ProtocolTCP {
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
}
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)
o, err := f.Get("v1/pods", path, true, labels.Everything())
if err != nil {
return nil, "", err
return nil, nil, err
}
var pod v1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
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 {
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.GetTable().ShowDeleted()
log.Debug().Msgf("SELS %v", selections)
for _, path := range selections {
if err := nuker.Delete(path, true, true); err != nil {
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 {
paths := r.GetTable().GetSelectedItems()
if len(paths) == 0 {
if len(paths) == 0 || paths[0] == "" {
return nil
}

View File

@ -36,26 +36,30 @@ func (s *ScaleExtender) bindKeys(aa ui.KeyActions) {
}
func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey {
path := s.GetTable().GetSelectedItem()
if path == "" {
paths := s.GetTable().GetSelectedItems()
if len(paths) == 0 {
return nil
}
s.Stop()
defer s.Start()
s.showScaleDialog(path)
s.showScaleDialog(paths)
return nil
}
func (s *ScaleExtender) showScaleDialog(path string) {
form, err := s.makeScaleForm(path)
func (s *ScaleExtender) showScaleDialog(paths []string) {
form, err := s.makeScaleForm(paths)
if err != nil {
s.App().Flash().Err(err)
return
}
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) {
s.dismissDialog()
})
@ -71,40 +75,49 @@ func (s *ScaleExtender) valueOf(col string) (string, error) {
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()
replicas, err := s.valueOf("READY")
if err != nil {
return nil, err
factor := "0"
if len(sels) == 1 {
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, "/")
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 {
f.AddInputField("Replicas:", factor, 4, func(textToCheck string, lastChar rune) bool {
_, err := strconv.Atoi(textToCheck)
return err == nil
}, func(changed string) {
replicas = changed
factor = changed
})
f.AddButton("OK", func() {
defer s.dismissDialog()
count, err := strconv.Atoi(replicas)
count, err := strconv.Atoi(factor)
if err != nil {
s.App().Flash().Err(err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
defer cancel()
if err := s.scale(ctx, sel, count); err != nil {
log.Error().Err(err).Msgf("DP %s scaling failed", sel)
s.App().Flash().Err(err)
return
for _, sel := range sels {
if err := s.scale(ctx, sel, count); err != nil {
log.Error().Err(err).Msgf("DP %s scaling failed", sel)
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() {

View File

@ -2,6 +2,7 @@ package view
import (
"context"
"strings"
"time"
"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.
func (t *Table) HeaderIndex(header string) (int, bool) {
func (t *Table) HeaderIndex(colName string) (int, bool) {
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
}
}

View File

@ -2,7 +2,7 @@ package view
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
@ -25,10 +25,10 @@ func TestTableSave(t *testing.T) {
v.SetTitle("k9s-test")
dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir)
c1, _ := os.ReadDir(dir)
v.saveCmd(nil)
c2, _ := ioutil.ReadDir(dir)
c2, _ := os.ReadDir(dir)
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) {
f.mx.Lock()
defer f.mx.Unlock()
f.forwarders[pf.Path()] = pf
for k, v := range f.forwarders {
log.Debug().Msgf("%q -- %#v", k, v)
}
f.forwarders[pf.Path()] = pf
}
// DeleteForwarder deletes portforward for a given container.
@ -277,11 +274,20 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) {
return fwd, ok
}
// BOZO!! Review!!!
// ValidatePortForwards check if pods are still around for portforwards.
func (f *Factory) ValidatePortForwards() {
for k, fwd := range f.forwarders {
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 {
fwd.Stop()
delete(f.forwarders, k)

View File

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

View File

@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"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 {
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err)
var o unstructured.Unstructured

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