rel 0.25.0
commit
e4eaf4d047
|
|
@ -278,7 +278,8 @@ K9s uses aliases to navigate most K8s resources.
|
||||||
|
|
||||||
## K9s Configuration
|
## K9s Configuration
|
||||||
|
|
||||||
K9s keeps its configurations inside of a `k9s` directory and the location depends on your operating system. K9s leverages [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) to load its various configurations files.
|
K9s keeps its configurations inside of a `k9s` directory and the location depends on your operating system. K9s leverages [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) to load its various configurations files. For information on the default locations for your OS please see [this link](https://github.com/adrg/xdg/blob/master/README.md). If you are still confused a quick `k9s info` will reveal where k9s is loading its configurations from.
|
||||||
|
|
||||||
|
|
||||||
| Unix | macOS | Windows |
|
| Unix | macOS | Windows |
|
||||||
|-----------------|-----------------------------|-----------------------|
|
|-----------------|-----------------------------|-----------------------|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv
|
||||||
|
|
||||||
## ♫ Sounds Behind The Release ♭
|
## ♫ Sounds Behind The Release ♭
|
||||||
|
|
||||||
* [High Fidelity - By Elvis Costello (yup! he started as a computer operator. Can u tell?)](https://www.youtube.com/watch?v=DJS-2kacmpU)
|
* [High Fidelity - By Elvis Costello (Yup! he started is career as a computer operator. Can u tell??)](https://www.youtube.com/watch?v=DJS-2kacmpU)
|
||||||
* [Walk With A Big Stick - Foster The People](https://www.youtube.com/watch?v=XMY1VMTyl8s)
|
* [Walk With A Big Stick - Foster The People](https://www.youtube.com/watch?v=XMY1VMTyl8s)
|
||||||
* [Beirut - Steps Ahead -- Love this band!! with the ever so talented and sadly late Michael Brecker ;(](https://www.youtube.com/watch?v=UExKTZ3veB8)
|
* [Beirut - Steps Ahead -- Love this band!! with the ever so talented and sadly late Michael Brecker ;(](https://www.youtube.com/watch?v=UExKTZ3veB8)
|
||||||
|
|
||||||
|
|
@ -25,20 +25,31 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv
|
||||||
I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!
|
I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!
|
||||||
|
|
||||||
* [Andrew Regan](https://github.com/poblish)
|
* [Andrew Regan](https://github.com/poblish)
|
||||||
* [Astraea](https://github.com/s22s)
|
* [Bruno Brito](https://github.com/brunohbrito)
|
||||||
* [DataRoots](https://github.com/datarootsio)
|
* [ScubaDrew](https://github.com/ScubaDrew)
|
||||||
|
* [mike-code](https://github.com/mike-code)
|
||||||
|
* [Andrew Aadland](https://github.com/DaemonDude23)
|
||||||
|
* [Michael Albers](https://github.com/michaeljohnalbers)
|
||||||
|
|
||||||
So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large.
|
So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large.
|
||||||
|
|
||||||
|
Also please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!
|
||||||
|
|
||||||
Thank you!!
|
Thank you!!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Forward That!
|
## Personal Note...
|
||||||
|
|
||||||
Ever been in a situation where you need to constantly port-forward on a given pod with multiple containers exposing multiple ports? If so it might be cumbersome to have to type in the full container:port specification to activate a forward. If you fall in this use cases, you can now specify which container and port you would rather port-forward to by default. In this drop, we introduce a new annotation that you can use to specify and container/port to forward to by default. If set the port-forward dialog will know default to your settings.
|
I had so many distractions this cycle so expect some `disturbance in the farce!` on this drop.
|
||||||
|
To boot rat holed quiet a bit on improving speed. So I might have drop some stuff on the floor in the process...
|
||||||
|
Please report back if that's the case and we will address shortly. Tx!!
|
||||||
|
|
||||||
> NOTE: you can either use a port name or number in your annotation.
|
## Port It Forward??
|
||||||
|
|
||||||
|
Ever been in a situation where you need to constantly port-forward on a given pod with multiple containers or exposing multiple ports? If so it might be cumbersome to have to type in the full container:port specification to activate a forward. If you fall in this use cases, you can now specify which container and port you would rather port-forward to by default. In this drop, we introduce a new annotation that you can use to specify and container/port to forward to by default. If set, the port-forward dialog will know to default to your settings.
|
||||||
|
|
||||||
|
> NOTE: you can either use a container port name or number in your annotation!
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Pod fred
|
# Pod fred
|
||||||
|
|
@ -47,9 +58,10 @@ kind: Pod
|
||||||
metadata:
|
metadata:
|
||||||
name: fred
|
name: fred
|
||||||
annotations:
|
annotations:
|
||||||
k9s.imhotep.io/default-portforward-container: bozo:p1 # => will default to container bozo port named p1
|
k9scli.io/auto-portforwards: zorg::5556 # => will default to container zorg port 5556 and local port 5566. No port-forward dialog will be shown.
|
||||||
# Or...
|
# Or...
|
||||||
k9s.imhotep.io/default-portforward-container: bozo:8081 # => will default to container bozo port number 8081
|
k9scli.io/portforward: bozo::6666:p1 # => launches the port-forward dialog selecting default port-forward on container bozo port named p1(8081)
|
||||||
|
# mapping to local port 6666.
|
||||||
...
|
...
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
|
@ -67,18 +79,35 @@ spec:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The annotation value must specify a container to forward to as well as a local port and container port. The container port may be specified as either a port number or port name. If the local port is omitted then the local port will default to the container port number. Here are a few examples:
|
||||||
|
|
||||||
|
1. bozo::http - creates a pf on container `bozo` with port name http. If http specifies port number 8080 then the local port will be 8080 as well.
|
||||||
|
2. bozo::9090:http - creates a pf on container `bozo` mapping local port 9090->http(8080)
|
||||||
|
3. bozo::9090:8080 - creates a pf on container `bozo` mapping local port 9090->8080
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resolved Issues
|
## Resolved Issues
|
||||||
|
|
||||||
|
* [Issue #1299](https://github.com/derailed/k9s/issues/1299) After upgrade to 0.24.15 sorting shortcuts not working
|
||||||
|
* [Issue #1298](https://github.com/derailed/k9s/issues/1298) Install K9s through go get reporting ambiguous import error
|
||||||
|
* [Issue #1296](https://github.com/derailed/k9s/issues/1296) Crash when clicking between border of K9s and terminal pane
|
||||||
|
* [Issue #1289](https://github.com/derailed/k9s/issues/1289) Homebrew calling bottle :unneeded is deprecated! There is no replacement
|
||||||
|
* [Issue #1273](https://github.com/derailed/k9s/issues/1273) Not loading config from correct default location when XDG_CONFIG_HOME is unset
|
||||||
* [Issue #1268](https://github.com/derailed/k9s/issues/1268) Age sorting wrong for years
|
* [Issue #1268](https://github.com/derailed/k9s/issues/1268) Age sorting wrong for years
|
||||||
* [Issue #1258](https://github.com/derailed/k9s/issues/1258) Configurable or recent use based port-forward
|
* [Issue #1258](https://github.com/derailed/k9s/issues/1258) Configurable or recent use based port-forward
|
||||||
* [Issue #1257](https://github.com/derailed/k9s/issues/1257) Why is the latest chocolatey on 0.24.10
|
* [Issue #1257](https://github.com/derailed/k9s/issues/1257) Why is the latest chocolatey on 0.24.10
|
||||||
|
* [Issue #1243](https://github.com/derailed/k9s/issues/1243) Port forward fails in kind on windows 10
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PRs
|
## PRs
|
||||||
|
|
||||||
|
* [PR #1300](https://github.com/derailed/k9s/pull/1300) move from io/ioutil to io/os packages
|
||||||
|
* [PR #1287](https://github.com/derailed/k9s/pull/1287) Add missing styles to kiss
|
||||||
|
* [PR #1286](https://github.com/derailed/k9s/pull/1286) Some small color modifications
|
||||||
|
* [PR #1284](https://github.com/derailed/k9s/pull/1284) Fix a small typo which comes from cluster view info
|
||||||
|
* [PR #1271](https://github.com/derailed/k9s/pull/1271) Removed cursor colors that are too light to read
|
||||||
* [PR #1266](https://github.com/derailed/k9s/pull/1266) Skin to preserve your terminal session background color
|
* [PR #1266](https://github.com/derailed/k9s/pull/1266) Skin to preserve your terminal session background color
|
||||||
* [PR #1264](https://github.com/derailed/k9s/pull/1205) Adding note on popeye config
|
* [PR #1264](https://github.com/derailed/k9s/pull/1205) Adding note on popeye config
|
||||||
* [PR #1261](https://github.com/derailed/k9s/pull/1261) Blurry logo
|
* [PR #1261](https://github.com/derailed/k9s/pull/1261) Blurry logo
|
||||||
|
|
|
||||||
26
cmd/root.go
26
cmd/root.go
|
|
@ -94,25 +94,25 @@ func loadConfiguration() *config.Config {
|
||||||
k9sCfg.K9s.OverrideWrite(*k9sFlags.Write)
|
k9sCfg.K9s.OverrideWrite(*k9sFlags.Write)
|
||||||
k9sCfg.K9s.OverrideCommand(*k9sFlags.Command)
|
k9sCfg.K9s.OverrideCommand(*k9sFlags.Command)
|
||||||
|
|
||||||
if err := k9sCfg.Refine(k8sFlags, k9sFlags); err != nil {
|
if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
|
||||||
log.Error().Err(err).Msgf("refine failed")
|
log.Error().Err(err).Msgf("refine failed")
|
||||||
}
|
}
|
||||||
conn, err := client.InitConnection(k8sCfg)
|
conn, err := client.InitConnection(k8sCfg)
|
||||||
k9sCfg.SetConnection(conn)
|
k9sCfg.SetConnection(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("failed to connect to cluster")
|
log.Error().Err(err).Msgf("failed to connect to cluster")
|
||||||
} else {
|
return k9sCfg
|
||||||
// Try to access server version if that fail. Connectivity issue?
|
}
|
||||||
if !k9sCfg.GetConnection().CheckConnectivity() {
|
// Try to access server version if that fail. Connectivity issue?
|
||||||
log.Panic().Msgf("K9s can't connect to cluster")
|
if !k9sCfg.GetConnection().CheckConnectivity() {
|
||||||
}
|
log.Panic().Msgf("Cannot connect to cluster")
|
||||||
if !k9sCfg.GetConnection().ConnectionOK() {
|
}
|
||||||
panic("No connectivity")
|
if !k9sCfg.GetConnection().ConnectionOK() {
|
||||||
}
|
panic("No connectivity")
|
||||||
log.Info().Msg("✅ Kubernetes connectivity")
|
}
|
||||||
if err := k9sCfg.Save(); err != nil {
|
log.Info().Msg("✅ Kubernetes connectivity")
|
||||||
log.Error().Err(err).Msg("Config save")
|
if err := k9sCfg.Save(); err != nil {
|
||||||
}
|
log.Error().Err(err).Msg("Config save")
|
||||||
}
|
}
|
||||||
|
|
||||||
return k9sCfg
|
return k9sCfg
|
||||||
|
|
|
||||||
6
go.mod
6
go.mod
|
|
@ -14,7 +14,7 @@ require (
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible
|
github.com/cenkalti/backoff v2.2.1+incompatible
|
||||||
github.com/cenkalti/backoff/v4 v4.1.1
|
github.com/cenkalti/backoff/v4 v4.1.1
|
||||||
github.com/derailed/popeye v0.9.7
|
github.com/derailed/popeye v0.9.7
|
||||||
github.com/derailed/tview v0.6.1
|
github.com/derailed/tview v0.6.3
|
||||||
github.com/fatih/color v1.12.0
|
github.com/fatih/color v1.12.0
|
||||||
github.com/fsnotify/fsnotify v1.5.1
|
github.com/fsnotify/fsnotify v1.5.1
|
||||||
github.com/fvbommel/sortorder v1.0.2
|
github.com/fvbommel/sortorder v1.0.2
|
||||||
|
|
@ -22,10 +22,6 @@ require (
|
||||||
github.com/ghodss/yaml v1.0.0
|
github.com/ghodss/yaml v1.0.0
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13
|
github.com/mattn/go-runewidth v0.0.13
|
||||||
// BOZO!! revamp with latest...
|
|
||||||
// github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
|
||||||
// github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
|
||||||
// github.com/openfaas/faas-provider v0.15.0
|
|
||||||
github.com/petergtz/pegomock v2.9.0+incompatible
|
github.com/petergtz/pegomock v2.9.0+incompatible
|
||||||
github.com/rakyll/hey v0.1.4
|
github.com/rakyll/hey v0.1.4
|
||||||
github.com/rs/zerolog v1.25.0
|
github.com/rs/zerolog v1.25.0
|
||||||
|
|
|
||||||
5
go.sum
5
go.sum
|
|
@ -216,8 +216,8 @@ github.com/derailed/popeye v0.9.7 h1:EnOl8rwvAlN4KJo62+V7J713ZAWFQmKCrTBBBBBkbmQ
|
||||||
github.com/derailed/popeye v0.9.7/go.mod h1:Ih3wTG7wBOuxdqz5tlCuCFq/vyB+Te/IpqY5HwgUTEA=
|
github.com/derailed/popeye v0.9.7/go.mod h1:Ih3wTG7wBOuxdqz5tlCuCFq/vyB+Te/IpqY5HwgUTEA=
|
||||||
github.com/derailed/tcell/v2 v2.3.1-rc.2 h1:9TmZB/IwL3MA1Jf4pC4rfMaPTcVYIN62IwE7X7A9emU=
|
github.com/derailed/tcell/v2 v2.3.1-rc.2 h1:9TmZB/IwL3MA1Jf4pC4rfMaPTcVYIN62IwE7X7A9emU=
|
||||||
github.com/derailed/tcell/v2 v2.3.1-rc.2/go.mod h1:wegJ+SscH+jPjEQIAV/dI/grLTRm5R4IE2M479NDSL0=
|
github.com/derailed/tcell/v2 v2.3.1-rc.2/go.mod h1:wegJ+SscH+jPjEQIAV/dI/grLTRm5R4IE2M479NDSL0=
|
||||||
github.com/derailed/tview v0.6.1 h1:dB+9bO7r6a1Yg1HE+XNJj61hioauJnGBFq2biC5bjAk=
|
github.com/derailed/tview v0.6.3 h1:4GFzcmuVjHYHKlLEpU8lSiUBVfHeYQEC0z5tlBLp4CI=
|
||||||
github.com/derailed/tview v0.6.1/go.mod h1:5Wjopun0Jw3zxOFtafwc/GlrkFJix1hZz1oQetWpnwE=
|
github.com/derailed/tview v0.6.3/go.mod h1:j2GwRsCb3NZe7lRjKIeplvZLkg8duyNWG6I4y+bZwEE=
|
||||||
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
|
@ -1144,7 +1144,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ func InitConnection(config *Config) (*APIClient, error) {
|
||||||
}
|
}
|
||||||
err := a.supportsMetricsResources()
|
err := a.supportsMetricsResources()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Checking metrics-server")
|
log.Error().Err(err).Msgf("Fail to locate metrics-server")
|
||||||
}
|
}
|
||||||
if errors.Is(err, noMetricServerErr) || errors.Is(err, metricsUnsupportedErr) {
|
if errors.Is(err, noMetricServerErr) || errors.Is(err, metricsUnsupportedErr) {
|
||||||
return &a, nil
|
return &a, nil
|
||||||
|
|
@ -120,11 +120,11 @@ func (a *APIClient) IsActiveNamespace(ns string) bool {
|
||||||
|
|
||||||
// ActiveNamespace returns the current namespace.
|
// ActiveNamespace returns the current namespace.
|
||||||
func (a *APIClient) ActiveNamespace() string {
|
func (a *APIClient) ActiveNamespace() string {
|
||||||
ns, err := a.CurrentNamespaceName()
|
if ns, err := a.CurrentNamespaceName(); err == nil {
|
||||||
if err != nil {
|
return ns
|
||||||
return AllNamespaces
|
|
||||||
}
|
}
|
||||||
return ns
|
|
||||||
|
return AllNamespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *APIClient) clearCache() {
|
func (a *APIClient) clearCache() {
|
||||||
|
|
@ -261,7 +261,7 @@ func (a *APIClient) CheckConnectivity() bool {
|
||||||
a.reset()
|
a.reset()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Error().Err(err).Msgf("K9s can't connect to cluster")
|
log.Error().Err(err).Msgf("can't connect to cluster")
|
||||||
a.connOK = false
|
a.connOK = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,11 +301,7 @@ func (a *APIClient) Dial() (kubernetes.Interface, error) {
|
||||||
|
|
||||||
// RestConfig returns a rest api client.
|
// RestConfig returns a rest api client.
|
||||||
func (a *APIClient) RestConfig() (*restclient.Config, error) {
|
func (a *APIClient) RestConfig() (*restclient.Config, error) {
|
||||||
cfg, err := a.config.RESTConfig()
|
return a.config.RESTConfig()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CachedDiscovery returns a cached discovery client.
|
// CachedDiscovery returns a cached discovery client.
|
||||||
|
|
@ -430,7 +426,6 @@ func (a *APIClient) supportsMetricsResources() error {
|
||||||
}
|
}
|
||||||
apiGroups, err := dial.ServerGroups()
|
apiGroups, err := dial.ServerGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msgf("Unable to fetch APIGroups")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, grp := range apiGroups.Groups {
|
for _, grp := range apiGroups.Groups {
|
||||||
|
|
|
||||||
|
|
@ -15,32 +15,30 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultQPS = 50
|
|
||||||
defaultBurst = 50
|
|
||||||
defaultCallTimeoutDuration time.Duration = 5 * time.Second
|
defaultCallTimeoutDuration time.Duration = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config tracks a kubernetes configuration.
|
// Config tracks a kubernetes configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
flags *genericclioptions.ConfigFlags
|
flags *genericclioptions.ConfigFlags
|
||||||
clientConfig clientcmd.ClientConfig
|
clientCfg clientcmd.ClientConfig
|
||||||
rawConfig *clientcmdapi.Config
|
rawCfg *clientcmdapi.Config
|
||||||
restConfig *restclient.Config
|
mutex *sync.RWMutex
|
||||||
mutex *sync.RWMutex
|
OverrideNS bool
|
||||||
OverrideNS bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig returns a new k8s config or an error if the flags are invalid.
|
// NewConfig returns a new k8s config or an error if the flags are invalid.
|
||||||
func NewConfig(f *genericclioptions.ConfigFlags) *Config {
|
func NewConfig(f *genericclioptions.ConfigFlags) *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
flags: f,
|
flags: f,
|
||||||
|
// pathOptions: clientcmd.NewDefaultPathOptions(),
|
||||||
mutex: &sync.RWMutex{},
|
mutex: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallTimeout returns the call timeout if set or the default if not set.
|
// CallTimeout returns the call timeout if set or the default if not set.
|
||||||
func (c *Config) CallTimeout() time.Duration {
|
func (c *Config) CallTimeout() time.Duration {
|
||||||
if c.flags.Timeout == nil {
|
if !isSet(c.flags.Timeout) {
|
||||||
return defaultCallTimeoutDuration
|
return defaultCallTimeoutDuration
|
||||||
}
|
}
|
||||||
dur, err := time.ParseDuration(*c.flags.Timeout)
|
dur, err := time.ParseDuration(*c.flags.Timeout)
|
||||||
|
|
@ -51,28 +49,63 @@ func (c *Config) CallTimeout() time.Duration {
|
||||||
return dur
|
return dur
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) RESTConfig() (*restclient.Config, error) {
|
||||||
|
return c.clientConfig().ClientConfig()
|
||||||
|
}
|
||||||
|
|
||||||
// Flags returns configuration flags.
|
// Flags returns configuration flags.
|
||||||
func (c *Config) Flags() *genericclioptions.ConfigFlags {
|
func (c *Config) Flags() *genericclioptions.ConfigFlags {
|
||||||
return c.flags
|
return c.flags
|
||||||
}
|
}
|
||||||
|
|
||||||
// SwitchContext changes the kubeconfig context to a new cluster.
|
func (c *Config) rawConfig() (*clientcmdapi.Config, error) {
|
||||||
func (c *Config) SwitchContext(name string) error {
|
if c.rawCfg != nil {
|
||||||
if c.flags.Context != nil && *c.flags.Context == name {
|
return c.rawCfg, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := c.GetContext(name); err != nil {
|
cfg, err := c.clientConfig().RawConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.rawCfg = &cfg
|
||||||
|
|
||||||
|
return c.rawCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) clientConfig() clientcmd.ClientConfig {
|
||||||
|
if c.clientCfg != nil {
|
||||||
|
return c.clientCfg
|
||||||
|
}
|
||||||
|
c.clientCfg = c.flags.ToRawKubeConfigLoader()
|
||||||
|
|
||||||
|
return c.clientCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) reset() {
|
||||||
|
c.clientCfg = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchContext changes the kubeconfig context to a new cluster.
|
||||||
|
func (c *Config) SwitchContext(name string) error {
|
||||||
|
cfg, err := c.rawConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cfg.CurrentContext == name {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
context, err := c.GetContext(name)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("context %s does not exist", name)
|
return fmt.Errorf("context %s does not exist", name)
|
||||||
}
|
}
|
||||||
c.reset()
|
|
||||||
c.flags.Context = &name
|
c.flags.Context = &name
|
||||||
|
c.flags.ClusterName = &(context.Cluster)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) reset() {
|
func (c *Config) RawConfig() *clientcmdapi.Config {
|
||||||
c.clientConfig, c.rawConfig, c.restConfig = nil, nil, nil
|
return c.rawCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentContextName returns the currently active config context.
|
// CurrentContextName returns the currently active config context.
|
||||||
|
|
@ -80,17 +113,17 @@ func (c *Config) CurrentContextName() (string, error) {
|
||||||
if isSet(c.flags.Context) {
|
if isSet(c.flags.Context) {
|
||||||
return *c.flags.Context, nil
|
return *c.flags.Context, nil
|
||||||
}
|
}
|
||||||
|
cfg, err := c.rawConfig()
|
||||||
cfg, err := c.RawConfig()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg.CurrentContext, nil
|
return cfg.CurrentContext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContext fetch a given context or error if it does not exists.
|
// GetContext fetch a given context or error if it does not exists.
|
||||||
func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) {
|
func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) {
|
||||||
cfg, err := c.RawConfig()
|
cfg, err := c.rawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +136,7 @@ func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) {
|
||||||
|
|
||||||
// Contexts fetch all available contexts.
|
// Contexts fetch all available contexts.
|
||||||
func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) {
|
func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) {
|
||||||
cfg, err := c.RawConfig()
|
cfg, err := c.rawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -113,18 +146,23 @@ func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) {
|
||||||
|
|
||||||
// DelContext remove a given context from the configuration.
|
// DelContext remove a given context from the configuration.
|
||||||
func (c *Config) DelContext(n string) error {
|
func (c *Config) DelContext(n string) error {
|
||||||
cfg, err := c.RawConfig()
|
cfg, err := c.rawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
delete(cfg.Contexts, n)
|
delete(cfg.Contexts, n)
|
||||||
|
|
||||||
return clientcmd.ModifyConfig(c.clientConfig.ConfigAccess(), cfg, true)
|
acc, err := c.ConfigAccess()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientcmd.ModifyConfig(acc, *cfg, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextNames fetch all available contexts.
|
// ContextNames fetch all available contexts.
|
||||||
func (c *Config) ContextNames() ([]string, error) {
|
func (c *Config) ContextNames() ([]string, error) {
|
||||||
cfg, err := c.RawConfig()
|
cfg, err := c.rawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -137,16 +175,16 @@ func (c *Config) ContextNames() ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClusterNameFromContext returns the cluster associated with the given context.
|
// ClusterNameFromContext returns the cluster associated with the given context.
|
||||||
func (c *Config) ClusterNameFromContext(ctx string) (string, error) {
|
func (c *Config) ClusterNameFromContext(context string) (string, error) {
|
||||||
cfg, err := c.RawConfig()
|
cfg, err := c.rawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx, ok := cfg.Contexts[ctx]; ok {
|
if ctx, ok := cfg.Contexts[context]; ok {
|
||||||
return ctx.Cluster, nil
|
return ctx.Cluster, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("unable to locate cluster from context %s", ctx)
|
return "", fmt.Errorf("unable to locate cluster from context %s", context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentClusterName returns the active cluster name.
|
// CurrentClusterName returns the active cluster name.
|
||||||
|
|
@ -154,16 +192,11 @@ func (c *Config) CurrentClusterName() (string, error) {
|
||||||
if isSet(c.flags.ClusterName) {
|
if isSet(c.flags.ClusterName) {
|
||||||
return *c.flags.ClusterName, nil
|
return *c.flags.ClusterName, nil
|
||||||
}
|
}
|
||||||
|
cfg, err := c.rawConfig()
|
||||||
cfg, err := c.RawConfig()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
current := cfg.CurrentContext
|
current := cfg.CurrentContext
|
||||||
if isSet(c.flags.Context) {
|
|
||||||
current = *c.flags.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx, ok := cfg.Contexts[current]; ok {
|
if ctx, ok := cfg.Contexts[current]; ok {
|
||||||
return ctx.Cluster, nil
|
return ctx.Cluster, nil
|
||||||
|
|
@ -174,7 +207,7 @@ func (c *Config) CurrentClusterName() (string, error) {
|
||||||
|
|
||||||
// ClusterNames fetch all kubeconfig defined clusters.
|
// ClusterNames fetch all kubeconfig defined clusters.
|
||||||
func (c *Config) ClusterNames() ([]string, error) {
|
func (c *Config) ClusterNames() ([]string, error) {
|
||||||
cfg, err := c.RawConfig()
|
cfg, err := c.rawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +257,7 @@ func (c *Config) CurrentUserName() (string, error) {
|
||||||
return *c.flags.AuthInfoName, nil
|
return *c.flags.AuthInfoName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := c.RawConfig()
|
cfg, err := c.rawConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -242,25 +275,9 @@ func (c *Config) CurrentUserName() (string, error) {
|
||||||
|
|
||||||
// CurrentNamespaceName retrieves the active namespace.
|
// CurrentNamespaceName retrieves the active namespace.
|
||||||
func (c *Config) CurrentNamespaceName() (string, error) {
|
func (c *Config) CurrentNamespaceName() (string, error) {
|
||||||
if isSet(c.flags.Namespace) {
|
ns, _, err := c.clientConfig().Namespace()
|
||||||
return *c.flags.Namespace, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := c.RawConfig()
|
return ns, err
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
ctx, err := c.CurrentContextName()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if ctx, ok := cfg.Contexts[ctx]; ok {
|
|
||||||
if isSet(&ctx.Namespace) {
|
|
||||||
return ctx.Namespace, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("No active namespace specified")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NamespaceNames fetch all available namespaces on current cluster.
|
// NamespaceNames fetch all available namespaces on current cluster.
|
||||||
|
|
@ -278,51 +295,7 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
c.ensureConfig()
|
return c.clientConfig().ConfigAccess(), nil
|
||||||
return c.clientConfig.ConfigAccess(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawConfig fetch the current kubeconfig with no overrides.
|
|
||||||
func (c *Config) RawConfig() (clientcmdapi.Config, error) {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
if c.rawConfig == nil {
|
|
||||||
c.ensureConfig()
|
|
||||||
cfg, err := c.clientConfig.RawConfig()
|
|
||||||
if err != nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
c.rawConfig = &cfg
|
|
||||||
if c.flags.Context == nil {
|
|
||||||
c.flags.Context = &c.rawConfig.CurrentContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return *c.rawConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RESTConfig fetch the current REST api service connection.
|
|
||||||
func (c *Config) RESTConfig() (*restclient.Config, error) {
|
|
||||||
if c.restConfig != nil {
|
|
||||||
return c.restConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if c.restConfig, err = c.flags.ToRESTConfig(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.restConfig.QPS = defaultQPS
|
|
||||||
c.restConfig.Burst = defaultBurst
|
|
||||||
|
|
||||||
return c.restConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ensureConfig() {
|
|
||||||
if c.clientConfig != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.clientConfig = c.flags.ToRawKubeConfigLoader()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package client_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
|
@ -18,98 +17,151 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigCurrentContext(t *testing.T) {
|
func TestConfigCurrentContext(t *testing.T) {
|
||||||
name, kubeConfig := "blee", "./testdata/config"
|
var kubeConfig = "./testdata/config"
|
||||||
uu := []struct {
|
|
||||||
flags *genericclioptions.ConfigFlags
|
uu := map[string]struct {
|
||||||
context string
|
context string
|
||||||
|
e string
|
||||||
}{
|
}{
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"},
|
"default": {
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &name}, "blee"},
|
e: "fred",
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
context: "blee",
|
||||||
|
e: "blee",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for k := range uu {
|
||||||
cfg := client.NewConfig(u.flags)
|
u := uu[k]
|
||||||
ctx, err := cfg.CurrentContextName()
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
flags := genericclioptions.NewConfigFlags(false)
|
||||||
assert.Equal(t, u.context, ctx)
|
flags.KubeConfig = &kubeConfig
|
||||||
|
if u.context != "" {
|
||||||
|
flags.Context = &u.context
|
||||||
|
}
|
||||||
|
cfg := client.NewConfig(flags)
|
||||||
|
ctx, err := cfg.CurrentContextName()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, u.e, ctx)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigCurrentCluster(t *testing.T) {
|
func TestConfigCurrentCluster(t *testing.T) {
|
||||||
name, kubeConfig := "blee", "./testdata/config"
|
name, kubeConfig := "blee", "./testdata/config"
|
||||||
uu := []struct {
|
uu := map[string]struct {
|
||||||
flags *genericclioptions.ConfigFlags
|
flags *genericclioptions.ConfigFlags
|
||||||
cluster string
|
cluster string
|
||||||
}{
|
}{
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"},
|
"default": {
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name}, "blee"},
|
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},
|
||||||
|
cluster: "fred",
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name},
|
||||||
|
cluster: "blee",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for k := range uu {
|
||||||
cfg := client.NewConfig(u.flags)
|
u := uu[k]
|
||||||
ctx, err := cfg.CurrentClusterName()
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
cfg := client.NewConfig(u.flags)
|
||||||
assert.Equal(t, u.cluster, ctx)
|
ctx, err := cfg.CurrentClusterName()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, u.cluster, ctx)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigCurrentUser(t *testing.T) {
|
func TestConfigCurrentUser(t *testing.T) {
|
||||||
name, kubeConfig := "blee", "./testdata/config"
|
name, kubeConfig := "blee", "./testdata/config"
|
||||||
uu := []struct {
|
uu := map[string]struct {
|
||||||
flags *genericclioptions.ConfigFlags
|
flags *genericclioptions.ConfigFlags
|
||||||
user string
|
user string
|
||||||
}{
|
}{
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "fred"},
|
"default": {
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, AuthInfoName: &name}, "blee"},
|
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},
|
||||||
|
user: "fred",
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, AuthInfoName: &name},
|
||||||
|
user: "blee",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for k := range uu {
|
||||||
cfg := client.NewConfig(u.flags)
|
u := uu[k]
|
||||||
ctx, err := cfg.CurrentUserName()
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
cfg := client.NewConfig(u.flags)
|
||||||
assert.Equal(t, u.user, ctx)
|
ctx, err := cfg.CurrentUserName()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, u.user, ctx)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigCurrentNamespace(t *testing.T) {
|
func TestConfigCurrentNamespace(t *testing.T) {
|
||||||
name, kubeConfig := "blee", "./testdata/config"
|
kubeConfig := "./testdata/config"
|
||||||
uu := []struct {
|
bleeNS, bleeCTX := "blee", "blee"
|
||||||
|
uu := map[string]struct {
|
||||||
flags *genericclioptions.ConfigFlags
|
flags *genericclioptions.ConfigFlags
|
||||||
namespace string
|
namespace string
|
||||||
err error
|
|
||||||
}{
|
}{
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "", fmt.Errorf("No active namespace specified")},
|
"default": {
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &name}, "blee", nil},
|
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},
|
||||||
|
namespace: "default",
|
||||||
|
},
|
||||||
|
"withContext": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &bleeCTX},
|
||||||
|
namespace: "zorg",
|
||||||
|
},
|
||||||
|
"withNS": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &bleeNS},
|
||||||
|
namespace: "blee",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
for k := range uu {
|
||||||
cfg := client.NewConfig(u.flags)
|
u := uu[k]
|
||||||
ns, err := cfg.CurrentNamespaceName()
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Equal(t, u.err, err)
|
cfg := client.NewConfig(u.flags)
|
||||||
assert.Equal(t, u.namespace, ns)
|
ns, err := cfg.CurrentNamespaceName()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, u.namespace, ns)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigGetContext(t *testing.T) {
|
func TestConfigGetContext(t *testing.T) {
|
||||||
kubeConfig := "./testdata/config"
|
kubeConfig := "./testdata/config"
|
||||||
uu := []struct {
|
uu := map[string]struct {
|
||||||
flags *genericclioptions.ConfigFlags
|
|
||||||
cluster string
|
cluster string
|
||||||
err error
|
err error
|
||||||
}{
|
}{
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "blee", nil},
|
"default": {
|
||||||
{&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "bozo", errors.New("invalid context `bozo specified")},
|
cluster: "blee",
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
cluster: "bozo",
|
||||||
|
err: errors.New("invalid context `bozo specified"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range uu {
|
flags := &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}
|
||||||
cfg := client.NewConfig(u.flags)
|
cfg := client.NewConfig(flags)
|
||||||
ctx, err := cfg.GetContext(u.cluster)
|
for k := range uu {
|
||||||
if err != nil {
|
u := uu[k]
|
||||||
assert.Equal(t, u.err, err)
|
t.Run(k, func(t *testing.T) {
|
||||||
} else {
|
ctx, err := cfg.GetContext(u.cluster)
|
||||||
assert.NotNil(t, ctx)
|
if err != nil {
|
||||||
assert.Equal(t, u.cluster, ctx.Cluster)
|
assert.Equal(t, u.err, err)
|
||||||
}
|
} else {
|
||||||
|
assert.NotNil(t, ctx)
|
||||||
|
assert.Equal(t, u.cluster, ctx.Cluster)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +257,8 @@ func TestConfigDelContext(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
cc, err := cfg.ContextNames()
|
cc, err := cfg.ContextNames()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 2, len(cc))
|
assert.Equal(t, 1, len(cc))
|
||||||
|
assert.Equal(t, "blee", cc[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigRestConfig(t *testing.T) {
|
func TestConfigRestConfig(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -2,42 +2,43 @@ apiVersion: v1
|
||||||
kind: Config
|
kind: Config
|
||||||
preferences: {}
|
preferences: {}
|
||||||
clusters:
|
clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
insecure-skip-tls-verify: true
|
insecure-skip-tls-verify: true
|
||||||
server: https://localhost:3000
|
server: https://localhost:3000
|
||||||
name: fred
|
name: fred
|
||||||
- cluster:
|
- cluster:
|
||||||
insecure-skip-tls-verify: true
|
insecure-skip-tls-verify: true
|
||||||
server: https://localhost:3001
|
server: https://localhost:3001
|
||||||
name: blee
|
name: blee
|
||||||
- cluster:
|
- cluster:
|
||||||
insecure-skip-tls-verify: true
|
insecure-skip-tls-verify: true
|
||||||
server: https://localhost:3002
|
server: https://localhost:3002
|
||||||
name: duh
|
name: duh
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
||||||
cluster: fred
|
cluster: fred
|
||||||
user: fred
|
user: fred
|
||||||
name: fred
|
name: fred
|
||||||
- context:
|
- context:
|
||||||
cluster: blee
|
cluster: blee
|
||||||
user: blee
|
user: blee
|
||||||
name: blee
|
namespace: zorg
|
||||||
- context:
|
name: blee
|
||||||
cluster: duh
|
- context:
|
||||||
user: duh
|
cluster: duh
|
||||||
name: duh
|
user: duh
|
||||||
|
name: duh
|
||||||
current-context: fred
|
current-context: fred
|
||||||
users:
|
users:
|
||||||
- name: fred
|
- name: fred
|
||||||
user:
|
user:
|
||||||
client-certificate-data: ZnJlZA==
|
client-certificate-data: ZnJlZA==
|
||||||
client-key-data: ZnJlZA==
|
client-key-data: ZnJlZA==
|
||||||
- name: blee
|
- name: blee
|
||||||
user:
|
user:
|
||||||
client-certificate-data: ZnJlZA==
|
client-certificate-data: ZnJlZA==
|
||||||
client-key-data: ZnJlZA==
|
client-key-data: ZnJlZA==
|
||||||
- name: duh
|
- name: duh
|
||||||
user:
|
user:
|
||||||
client-certificate-data: ZnJlZA==
|
client-certificate-data: ZnJlZA==
|
||||||
client-key-data: ZnJlZA==
|
client-key-data: ZnJlZA==
|
||||||
|
|
|
||||||
|
|
@ -7,33 +7,13 @@ clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
insecure-skip-tls-verify: true
|
insecure-skip-tls-verify: true
|
||||||
server: https://localhost:3002
|
server: https://localhost:3002
|
||||||
name: duh
|
|
||||||
- cluster:
|
|
||||||
insecure-skip-tls-verify: true
|
|
||||||
server: https://localhost:3000
|
|
||||||
name: fred
|
name: fred
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
||||||
cluster: blee
|
cluster: blee
|
||||||
user: blee
|
user: blee
|
||||||
name: blee
|
name: blee
|
||||||
- context:
|
current-context: blee
|
||||||
cluster: duh
|
|
||||||
user: duh
|
|
||||||
name: duh
|
|
||||||
current-context: fred
|
|
||||||
kind: Config
|
kind: Config
|
||||||
preferences: {}
|
preferences: {}
|
||||||
users:
|
users: null
|
||||||
- name: blee
|
|
||||||
user:
|
|
||||||
client-certificate-data: ZnJlZA==
|
|
||||||
client-key-data: ZnJlZA==
|
|
||||||
- name: duh
|
|
||||||
user:
|
|
||||||
client-certificate-data: ZnJlZA==
|
|
||||||
client-key-data: ZnJlZA==
|
|
||||||
- name: fred
|
|
||||||
user:
|
|
||||||
client-certificate-data: ZnJlZA==
|
|
||||||
client-key-data: ZnJlZA==
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ func (a *Aliases) Load() error {
|
||||||
|
|
||||||
// LoadFileAliases loads alias from a given file.
|
// LoadFileAliases loads alias from a given file.
|
||||||
func (a *Aliases) LoadFileAliases(path string) error {
|
func (a *Aliases) LoadFileAliases(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := os.ReadFile(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var aa Aliases
|
var aa Aliases
|
||||||
if err := yaml.Unmarshal(f, &aa); err != nil {
|
if err := yaml.Unmarshal(f, &aa); err != nil {
|
||||||
|
|
@ -171,5 +171,5 @@ func (a *Aliases) SaveAliases(path string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return ioutil.WriteFile(path, cfg, 0644)
|
return os.WriteFile(path, cfg, 0644)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
@ -96,7 +96,7 @@ func (s *Bench) Reload(path string) error {
|
||||||
|
|
||||||
// Load K9s benchmark configs from file.
|
// Load K9s benchmark configs from file.
|
||||||
func (s *Bench) load(path string) error {
|
func (s *Bench) load(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ package config
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
|
|
@ -60,6 +60,15 @@ func K9sHome() string {
|
||||||
if env := os.Getenv(K9sConfig); env != "" {
|
if env := os.Getenv(K9sConfig); env != "" {
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
if env := os.Getenv("XDG_CONFIG_HOME"); env == "" {
|
||||||
|
dir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("user home dir")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path.Join(dir, ".config", "k9s")
|
||||||
|
}
|
||||||
|
|
||||||
xdgK9sHome, err := xdg.ConfigFile("k9s")
|
xdgK9sHome, err := xdg.ConfigFile("k9s")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s")
|
log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s")
|
||||||
|
|
@ -74,21 +83,25 @@ func NewConfig(ks KubeSettings) *Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refine the configuration based on cli args.
|
// Refine the configuration based on cli args.
|
||||||
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags) error {
|
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error {
|
||||||
cfg, err := flags.ToRawKubeConfigLoader().RawConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isSet(flags.Context) {
|
if isSet(flags.Context) {
|
||||||
c.K9s.CurrentContext = *flags.Context
|
c.K9s.CurrentContext = *flags.Context
|
||||||
} else {
|
} else {
|
||||||
c.K9s.CurrentContext = cfg.CurrentContext
|
context, err := cfg.CurrentContextName()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.K9s.CurrentContext = context
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext)
|
log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext)
|
||||||
if c.K9s.CurrentContext == "" {
|
if c.K9s.CurrentContext == "" {
|
||||||
return errors.New("Invalid kubeconfig context detected")
|
return errors.New("Invalid kubeconfig context detected")
|
||||||
}
|
}
|
||||||
context, ok := cfg.Contexts[c.K9s.CurrentContext]
|
cc, err := cfg.Contexts()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
context, ok := cc[c.K9s.CurrentContext]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext)
|
return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext)
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +231,7 @@ func (c *Config) SetConnection(conn client.Connection) {
|
||||||
|
|
||||||
// Load K9s configuration from file.
|
// Load K9s configuration from file.
|
||||||
func (c *Config) Load(path string) error {
|
func (c *Config) Load(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +265,7 @@ func (c *Config) SaveFile(path string) error {
|
||||||
log.Error().Msgf("[Config] Unable to save K9s config file: %v", err)
|
log.Error().Msgf("[Config] Unable to save K9s config file: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return ioutil.WriteFile(path, cfg, 0644)
|
return os.WriteFile(path, cfg, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the configuration.
|
// Validate the configuration.
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
m "github.com/petergtz/pegomock"
|
m "github.com/petergtz/pegomock"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
@ -63,7 +64,7 @@ func TestConfigRefine(t *testing.T) {
|
||||||
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
|
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
|
||||||
cfg := config.NewConfig(mk)
|
cfg := config.NewConfig(mk)
|
||||||
|
|
||||||
err := cfg.Refine(u.flags, nil)
|
err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
|
||||||
if u.issue {
|
if u.issue {
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -87,7 +88,6 @@ func TestConfigValidate(t *testing.T) {
|
||||||
cfg.SetConnection(mc)
|
cfg.SetConnection(mc)
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
||||||
cfg.Validate()
|
cfg.Validate()
|
||||||
// mc.VerifyWasCalledOnce().ValidNamespaces()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigLoad(t *testing.T) {
|
func TestConfigLoad(t *testing.T) {
|
||||||
|
|
@ -216,7 +216,7 @@ func TestConfigSaveFile(t *testing.T) {
|
||||||
err := cfg.SaveFile(path)
|
err := cfg.SaveFile(path)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
raw, err := ioutil.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, expectedConfig, string(raw))
|
assert.Equal(t, expectedConfig, string(raw))
|
||||||
}
|
}
|
||||||
|
|
@ -242,7 +242,7 @@ func TestConfigReset(t *testing.T) {
|
||||||
err := cfg.SaveFile(path)
|
err := cfg.SaveFile(path)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
raw, err := ioutil.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, resetConfig, string(raw))
|
assert.Equal(t, resetConfig, string(raw))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
@ -36,7 +36,7 @@ func (h HotKeys) Load() error {
|
||||||
|
|
||||||
// LoadHotKeys loads plugins from a given file.
|
// LoadHotKeys loads plugins from a given file.
|
||||||
func (h HotKeys) LoadHotKeys(path string) error {
|
func (h HotKeys) LoadHotKeys(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
@ -20,12 +22,17 @@ type Plugin struct {
|
||||||
Scopes []string `yaml:"scopes"`
|
Scopes []string `yaml:"scopes"`
|
||||||
Args []string `yaml:"args"`
|
Args []string `yaml:"args"`
|
||||||
ShortCut string `yaml:"shortCut"`
|
ShortCut string `yaml:"shortCut"`
|
||||||
|
Pipes []string `yaml:"pipes"`
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
Command string `yaml:"command"`
|
Command string `yaml:"command"`
|
||||||
Confirm bool `yaml:"confirm"`
|
Confirm bool `yaml:"confirm"`
|
||||||
Background bool `yaml:"background"`
|
Background bool `yaml:"background"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Plugin) String() string {
|
||||||
|
return fmt.Sprintf("[%s] %s(%s)", p.ShortCut, p.Command, strings.Join(p.Args, " "))
|
||||||
|
}
|
||||||
|
|
||||||
// NewPlugins returns a new plugin.
|
// NewPlugins returns a new plugin.
|
||||||
func NewPlugins() Plugins {
|
func NewPlugins() Plugins {
|
||||||
return Plugins{
|
return Plugins{
|
||||||
|
|
@ -40,7 +47,7 @@ func (p Plugins) Load() error {
|
||||||
|
|
||||||
// LoadPlugins loads plugins from a given file.
|
// LoadPlugins loads plugins from a given file.
|
||||||
func (p Plugins) LoadPlugins(path string) error {
|
func (p Plugins) LoadPlugins(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -541,7 +541,7 @@ func (s *Styles) Views() Views {
|
||||||
|
|
||||||
// Load K9s configuration from file.
|
// Load K9s configuration from file.
|
||||||
func (s *Styles) Load(path string) error {
|
func (s *Styles) Load(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
@ -56,7 +56,7 @@ func (v *CustomView) Reset() {
|
||||||
|
|
||||||
// Load loads view configurations.
|
// Load loads view configurations.
|
||||||
func (v *CustomView) Load(path string) error {
|
func (v *CustomView) Load(path string) error {
|
||||||
raw, err := ioutil.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package dao
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -41,7 +40,7 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error
|
||||||
}
|
}
|
||||||
path, _ := ctx.Value(internal.KeyPath).(string)
|
path, _ := ctx.Value(internal.KeyPath).(string)
|
||||||
|
|
||||||
ff, err := ioutil.ReadDir(dir)
|
ff, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +50,10 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error
|
||||||
if path != "" && !strings.HasPrefix(f.Name(), strings.Replace(path, "/", "_", 1)) {
|
if path != "" && !strings.HasPrefix(f.Name(), strings.Replace(path, "/", "_", 1)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
oo = append(oo, render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())})
|
|
||||||
|
if fi, err := f.Info(); err == nil {
|
||||||
|
oo = append(oo, render.BenchInfo{File: fi, Path: filepath.Join(dir, f.Name())})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return oo, nil
|
return oo, nil
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
|
@ -63,14 +64,14 @@ func (c *Context) Switch(ctx string) error {
|
||||||
|
|
||||||
// KubeUpdate modifies kubeconfig default context.
|
// KubeUpdate modifies kubeconfig default context.
|
||||||
func (c *Context) KubeUpdate(n string) error {
|
func (c *Context) KubeUpdate(n string) error {
|
||||||
config, err := c.config().RawConfig()
|
cfg := c.config().RawConfig()
|
||||||
if err != nil {
|
if cfg == nil {
|
||||||
return err
|
return errors.New("unable to fetch raw config")
|
||||||
}
|
}
|
||||||
if err := c.Switch(n); err != nil {
|
if err := c.Switch(n); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return clientcmd.ModifyConfig(
|
return clientcmd.ModifyConfig(
|
||||||
clientcmd.NewDefaultPathOptions(), config, true,
|
clientcmd.NewDefaultPathOptions(), *cfg, true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package dao
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -29,7 +29,7 @@ func TestCruiserSlice(t *testing.T) {
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func loadJSON(t assert.TestingT, n string) *unstructured.Unstructured {
|
func loadJSON(t assert.TestingT, n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var o unstructured.Unstructured
|
var o unstructured.Unstructured
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package dao
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -37,7 +37,7 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
|
||||||
return nil, errors.New("No dir in context")
|
return nil, errors.New("No dir in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(dir)
|
files, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -48,8 +48,8 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
oo = append(oo, render.DirRes{
|
oo = append(oo, render.DirRes{
|
||||||
Path: filepath.Join(dir, f.Name()),
|
Path: filepath.Join(dir, f.Name()),
|
||||||
Info: f,
|
Entry: f,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,20 +69,20 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, _ := client.Namespaced(path)
|
auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", []string{client.PatchVerb})
|
||||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !auth {
|
if !auth {
|
||||||
return fmt.Errorf("user is not authorized to restart a deployment")
|
return fmt.Errorf("user is not authorized to restart a deployment")
|
||||||
}
|
}
|
||||||
update, err := polymorphichelpers.ObjectRestarterFn(dp)
|
|
||||||
|
dial, err := d.Client().Dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dial, err := d.Client().Dial()
|
restarter, err := polymorphichelpers.ObjectRestarterFn(dp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +91,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
|
||||||
ctx,
|
ctx,
|
||||||
dp.Name,
|
dp.Name,
|
||||||
types.StrategicMergePatchType,
|
types.StrategicMergePatchType,
|
||||||
update,
|
restarter,
|
||||||
metav1.PatchOptions{},
|
metav1.PatchOptions{},
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) e
|
||||||
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
|
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts *LogOptions) error {
|
func podLogs(ctx context.Context, out LogChan, sel map[string]string, opts *LogOptions) error {
|
||||||
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("expecting a context factory")
|
return errors.New("expecting a context factory")
|
||||||
|
|
@ -110,14 +110,13 @@ func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts *LogOpt
|
||||||
po := Pod{}
|
po := Pod{}
|
||||||
po.Init(f, client.NewGVR("v1/pods"))
|
po.Init(f, client.NewGVR("v1/pods"))
|
||||||
for _, o := range oo {
|
for _, o := range oo {
|
||||||
var pod v1.Pod
|
u, ok := o.(*unstructured.Unstructured)
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
if !ok {
|
||||||
if err != nil {
|
return fmt.Errorf("expected unstructured got %t", o)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
opts = opts.Clone()
|
opts = opts.Clone()
|
||||||
opts.Path = client.FQN(pod.Namespace, pod.Name)
|
opts.Path = client.FQN(u.GetNamespace(), u.GetName())
|
||||||
if err := po.TailLogs(ctx, c, opts); err != nil {
|
if err := po.TailLogs(ctx, out, opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,7 @@ func (c *Helm) Delete(path string, cascade, force bool) error {
|
||||||
// EnsureHelmConfig return a new configuration.
|
// EnsureHelmConfig return a new configuration.
|
||||||
func (c *Helm) EnsureHelmConfig(ns string) (*action.Configuration, error) {
|
func (c *Helm) EnsureHelmConfig(ns string) (*action.Configuration, error) {
|
||||||
cfg := new(action.Configuration)
|
cfg := new(action.Configuration)
|
||||||
flags := c.Client().Config().Flags()
|
if err := cfg.Init(c.Client().Config().Flags(), ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
|
||||||
if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
|
|
|
||||||
|
|
@ -2,39 +2,31 @@ package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/color"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LogChan represents a channel for logs.
|
// LogChan represents a channel for logs.
|
||||||
type LogChan chan *LogItem
|
type LogChan chan *LogItem
|
||||||
|
|
||||||
|
var ItemEOF = new(LogItem)
|
||||||
|
|
||||||
// LogItem represents a container log line.
|
// LogItem represents a container log line.
|
||||||
type LogItem struct {
|
type LogItem struct {
|
||||||
Pod, Container, Timestamp string
|
Pod, Container string
|
||||||
SingleContainer bool
|
SingleContainer bool
|
||||||
Bytes []byte
|
Bytes []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogItem returns a new item.
|
// NewLogItem returns a new item.
|
||||||
func NewLogItem(b []byte) *LogItem {
|
func NewLogItem(bb []byte) *LogItem {
|
||||||
space := []byte(" ")
|
|
||||||
cols := bytes.Split(b[:len(b)-1], space)
|
|
||||||
|
|
||||||
return &LogItem{
|
return &LogItem{
|
||||||
Timestamp: string(cols[0]),
|
Bytes: bb,
|
||||||
Bytes: bytes.Join(cols[1:], space),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogItemFromString returns a new item.
|
// NewLogItemFromString returns a new item.
|
||||||
func NewLogItemFromString(s string) *LogItem {
|
func NewLogItemFromString(s string) *LogItem {
|
||||||
return &LogItem{
|
return &LogItem{
|
||||||
Bytes: []byte(s),
|
Bytes: []byte(s),
|
||||||
Timestamp: time.Now().String(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,22 +38,18 @@ func (l *LogItem) ID() string {
|
||||||
return l.Container
|
return l.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone copies an item.
|
// GetTimestamp fetch log lime timestamp
|
||||||
func (l *LogItem) Clone() *LogItem {
|
func (l *LogItem) GetTimestamp() string {
|
||||||
bytes := make([]byte, len(l.Bytes))
|
index := bytes.Index(l.Bytes, []byte{' '})
|
||||||
copy(bytes, l.Bytes)
|
if index < 0 {
|
||||||
return &LogItem{
|
return ""
|
||||||
Container: l.Container,
|
|
||||||
Pod: l.Pod,
|
|
||||||
Timestamp: l.Timestamp,
|
|
||||||
SingleContainer: l.SingleContainer,
|
|
||||||
Bytes: bytes,
|
|
||||||
}
|
}
|
||||||
|
return string(l.Bytes[:index])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info returns pod and container information.
|
// Info returns pod and container information.
|
||||||
func (l *LogItem) Info() string {
|
func (l *LogItem) Info() string {
|
||||||
return fmt.Sprintf("%q::%q", l.Pod, l.Container)
|
return l.Pod + "::" + l.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEmpty checks if the entry is empty.
|
// IsEmpty checks if the entry is empty.
|
||||||
|
|
@ -69,37 +57,39 @@ func (l *LogItem) IsEmpty() bool {
|
||||||
return len(l.Bytes) == 0
|
return len(l.Bytes) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// Size returns the size of the item.
|
||||||
escPattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
|
func (l *LogItem) Size() int {
|
||||||
matcher = []byte("$1[]")
|
return 100 + len(l.Bytes) + len(l.Pod) + len(l.Container)
|
||||||
)
|
}
|
||||||
|
|
||||||
// Render returns a log line as string.
|
// Render returns a log line as string.
|
||||||
func (l *LogItem) Render(paint int, showTime bool) []byte {
|
func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) {
|
||||||
bb := make([]byte, 0, 200)
|
index := bytes.Index(l.Bytes, []byte{' '})
|
||||||
if showTime {
|
if showTime && index > 0 {
|
||||||
t := l.Timestamp
|
bb.WriteString("[gray::]")
|
||||||
for i := len(t); i < 30; i++ {
|
bb.Write(l.Bytes[:index])
|
||||||
t += " "
|
bb.WriteString(" ")
|
||||||
|
for i := len(l.Bytes[:index]); i < 30; i++ {
|
||||||
|
bb.WriteByte(' ')
|
||||||
}
|
}
|
||||||
bb = append(bb, color.ANSIColorize(t, 106)...)
|
|
||||||
bb = append(bb, ' ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasPod bool
|
|
||||||
if l.Pod != "" {
|
if l.Pod != "" {
|
||||||
bb = append(bb, color.ANSIColorize(l.Pod, paint)...)
|
bb.WriteString("[" + paint + "::]" + l.Pod)
|
||||||
hasPod = true
|
|
||||||
}
|
|
||||||
if !l.SingleContainer && l.Container != "" {
|
|
||||||
if hasPod {
|
|
||||||
bb = append(bb, ':')
|
|
||||||
}
|
|
||||||
bb = append(bb, color.ANSIColorize(l.Container, paint)...)
|
|
||||||
bb = append(bb, ' ')
|
|
||||||
} else if hasPod {
|
|
||||||
bb = append(bb, ' ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(bb, escPattern.ReplaceAll(l.Bytes, matcher)...)
|
if !l.SingleContainer && l.Container != "" {
|
||||||
|
if len(l.Pod) > 0 {
|
||||||
|
bb.WriteString(" ")
|
||||||
|
}
|
||||||
|
bb.WriteString("[" + paint + "::b]" + l.Container + "[-::-] ")
|
||||||
|
} else if len(l.Pod) > 0 {
|
||||||
|
bb.WriteString("[-::]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if index > 0 {
|
||||||
|
bb.Write(l.Bytes[index+1:])
|
||||||
|
} else {
|
||||||
|
bb.Write(l.Bytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package dao_test
|
package dao_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -34,13 +35,13 @@ func TestLogItemRender(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
"empty": {
|
"empty": {
|
||||||
opts: dao.LogOptions{},
|
opts: dao.LogOptions{},
|
||||||
e: "Testing 1,2,3...",
|
e: "Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
opts: dao.LogOptions{
|
opts: dao.LogOptions{
|
||||||
Container: "fred",
|
Container: "fred",
|
||||||
},
|
},
|
||||||
e: "\x1b[38;5;0mfred\x1b[0m Testing 1,2,3...",
|
e: "[yellow::b]fred[-::-] Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
"pod": {
|
"pod": {
|
||||||
opts: dao.LogOptions{
|
opts: dao.LogOptions{
|
||||||
|
|
@ -48,7 +49,7 @@ func TestLogItemRender(t *testing.T) {
|
||||||
Container: "blee",
|
Container: "blee",
|
||||||
SingleContainer: true,
|
SingleContainer: true,
|
||||||
},
|
},
|
||||||
e: "\x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...",
|
e: "[yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
"full": {
|
"full": {
|
||||||
opts: dao.LogOptions{
|
opts: dao.LogOptions{
|
||||||
|
|
@ -57,7 +58,7 @@ func TestLogItemRender(t *testing.T) {
|
||||||
SingleContainer: true,
|
SingleContainer: true,
|
||||||
ShowTimestamp: true,
|
ShowTimestamp: true,
|
||||||
},
|
},
|
||||||
e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00\x1b[0m \x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...",
|
e: "[gray::]2018-12-14T10:36:43.326972-07:00 [yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +70,9 @@ func TestLogItemRender(t *testing.T) {
|
||||||
_, n := client.Namespaced(u.opts.Path)
|
_, n := client.Namespaced(u.opts.Path)
|
||||||
i.Pod, i.Container = n, u.opts.Container
|
i.Pod, i.Container = n, u.opts.Container
|
||||||
|
|
||||||
assert.Equal(t, u.e, string(i.Render(0, u.opts.ShowTimestamp)))
|
bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
|
||||||
|
i.Render("yellow", u.opts.ShowTimestamp, bb)
|
||||||
|
assert.Equal(t, u.e, bb.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,9 +81,24 @@ func BenchmarkLogItemRender(b *testing.B) {
|
||||||
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
|
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
|
||||||
i := dao.NewLogItem(s)
|
i := dao.NewLogItem(s)
|
||||||
i.Pod, i.Container = "fred", "blee"
|
i.Pod, i.Container = "fred", "blee"
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
i.Render(0, true)
|
bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
|
||||||
|
i.Render("yellow", true, bb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLogItemRenderNoTS(b *testing.B) {
|
||||||
|
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
|
||||||
|
i := dao.NewLogItem(s)
|
||||||
|
i.Pod, i.Container = "fred", "blee"
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
|
||||||
|
i.Render("yellow", false, bb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
|
||||||
"github.com/sahilm/fuzzy"
|
"github.com/sahilm/fuzzy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var colorPalette = []tcell.Color{
|
var podPalette = []string{
|
||||||
tcell.ColorTeal,
|
"teal",
|
||||||
tcell.ColorGreen,
|
"green",
|
||||||
tcell.ColorPurple,
|
"purple",
|
||||||
tcell.ColorLime,
|
"lime",
|
||||||
tcell.ColorBlue,
|
"blue",
|
||||||
tcell.ColorYellow,
|
"yellow",
|
||||||
tcell.ColorFuchsia,
|
"fushia",
|
||||||
tcell.ColorAqua,
|
"aqua",
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogItems represents a collection of log items.
|
// LogItems represents a collection of log items.
|
||||||
type LogItems struct {
|
type LogItems struct {
|
||||||
items []*LogItem
|
items []*LogItem
|
||||||
colors map[string]tcell.Color
|
podColors map[string]string
|
||||||
mx sync.RWMutex
|
mx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogItems returns a new instance.
|
// NewLogItems returns a new instance.
|
||||||
func NewLogItems() *LogItems {
|
func NewLogItems() *LogItems {
|
||||||
return &LogItems{
|
return &LogItems{
|
||||||
colors: make(map[string]tcell.Color),
|
podColors: make(map[string]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,9 +56,9 @@ func (l *LogItems) Clear() {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
l.items = nil
|
l.items = l.items[:0]
|
||||||
for k := range l.colors {
|
for k := range l.podColors {
|
||||||
delete(l.colors, k)
|
delete(l.podColors, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,8 +76,8 @@ func (l *LogItems) Subset(index int) *LogItems {
|
||||||
defer l.mx.RUnlock()
|
defer l.mx.RUnlock()
|
||||||
|
|
||||||
return &LogItems{
|
return &LogItems{
|
||||||
items: l.items[index:],
|
items: l.items[index:],
|
||||||
colors: l.colors,
|
podColors: l.podColors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,8 +87,8 @@ func (l *LogItems) Merge(n *LogItems) {
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
l.items = append(l.items, n.items...)
|
l.items = append(l.items, n.items...)
|
||||||
for k, v := range n.colors {
|
for k, v := range n.podColors {
|
||||||
l.colors[k] = v
|
l.podColors[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,47 +101,60 @@ func (l *LogItems) Add(ii ...*LogItem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lines returns a collection of log lines.
|
// Lines returns a collection of log lines.
|
||||||
func (l *LogItems) Lines(showTime bool) [][]byte {
|
func (l *LogItems) Lines(index int, showTime bool, ll [][]byte) {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
ll := make([][]byte, len(l.items))
|
var colorIndex int
|
||||||
for i, item := range l.items {
|
for i, item := range l.items[index:] {
|
||||||
color := l.colors[item.ID()]
|
id := item.ID()
|
||||||
ll[i] = item.Render(int(color-tcell.ColorValid), showTime)
|
color, ok := l.podColors[id]
|
||||||
|
if !ok {
|
||||||
|
if colorIndex >= len(podPalette) {
|
||||||
|
colorIndex = 0
|
||||||
|
}
|
||||||
|
color = podPalette[colorIndex]
|
||||||
|
l.podColors[id] = color
|
||||||
|
colorIndex++
|
||||||
|
}
|
||||||
|
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
|
||||||
|
item.Render(color, showTime, bb)
|
||||||
|
ll[i] = bb.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ll
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StrLines returns a collection of log lines.
|
// StrLines returns a collection of log lines.
|
||||||
func (l *LogItems) StrLines(showTime bool) []string {
|
func (l *LogItems) StrLines(index int, showTime bool) []string {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
ll := make([]string, len(l.items))
|
ll := make([]string, len(l.items[index:]))
|
||||||
for i, item := range l.items {
|
for i, item := range l.items[index:] {
|
||||||
ll[i] = string(item.Render(0, showTime))
|
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
|
||||||
|
item.Render("white", showTime, bb)
|
||||||
|
ll[i] = bb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ll
|
return ll
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render returns logs as a collection of strings.
|
// Render returns logs as a collection of strings.
|
||||||
func (l *LogItems) Render(showTime bool, ll [][]byte) {
|
func (l *LogItems) Render(index int, showTime bool, ll [][]byte) {
|
||||||
index := len(l.colors)
|
var colorIndex int
|
||||||
for i, item := range l.items {
|
for i, item := range l.items[index:] {
|
||||||
id := item.ID()
|
id := item.ID()
|
||||||
color, ok := l.colors[id]
|
color, ok := l.podColors[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
if index >= len(colorPalette) {
|
if colorIndex >= len(podPalette) {
|
||||||
index = 0
|
colorIndex = 0
|
||||||
}
|
}
|
||||||
color = colorPalette[index]
|
color = podPalette[colorIndex]
|
||||||
l.colors[id] = color
|
l.podColors[id] = color
|
||||||
index++
|
colorIndex++
|
||||||
}
|
}
|
||||||
ll[i] = item.Render(int(color-tcell.ColorValid), showTime)
|
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
|
||||||
|
item.Render(color, showTime, bb)
|
||||||
|
ll[i] = bb.Bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,15 +167,15 @@ func (l *LogItems) DumpDebug(m string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter filters out log items based on given filter.
|
// Filter filters out log items based on given filter.
|
||||||
func (l *LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) {
|
func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, error) {
|
||||||
if q == "" {
|
if q == "" {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
if IsFuzzySelector(q) {
|
if IsFuzzySelector(q) {
|
||||||
mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:]), showTime)
|
mm, ii := l.fuzzyFilter(index, strings.TrimSpace(q[2:]), showTime)
|
||||||
return mm, ii, nil
|
return mm, ii, nil
|
||||||
}
|
}
|
||||||
matches, indices, err := l.filterLogs(q, showTime)
|
matches, indices, err := l.filterLogs(index, q, showTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -170,10 +183,10 @@ func (l *LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) {
|
||||||
return matches, indices, nil
|
return matches, indices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
|
func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]int) {
|
||||||
q = strings.TrimSpace(q)
|
q = strings.TrimSpace(q)
|
||||||
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10)
|
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10)
|
||||||
mm := fuzzy.Find(q, l.StrLines(showTime))
|
mm := fuzzy.Find(q, l.StrLines(index, showTime))
|
||||||
for _, m := range mm {
|
for _, m := range mm {
|
||||||
matches = append(matches, m.Index)
|
matches = append(matches, m.Index)
|
||||||
indices = append(indices, m.MatchedIndexes)
|
indices = append(indices, m.MatchedIndexes)
|
||||||
|
|
@ -182,7 +195,7 @@ func (l *LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
|
||||||
return matches, indices
|
return matches, indices
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
|
func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]int, error) {
|
||||||
var invert bool
|
var invert bool
|
||||||
if IsInverseSelector(q) {
|
if IsInverseSelector(q) {
|
||||||
invert = true
|
invert = true
|
||||||
|
|
@ -193,7 +206,9 @@ func (l *LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10)
|
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10)
|
||||||
for i, line := range l.Lines(showTime) {
|
ll := make([][]byte, len(l.items[index:]))
|
||||||
|
l.Lines(index, showTime, ll)
|
||||||
|
for i, line := range ll {
|
||||||
locs := rx.FindIndex(line)
|
locs := rx.FindIndex(line)
|
||||||
if locs != nil && invert {
|
if locs != nil && invert {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ func TestLogItemsFilter(t *testing.T) {
|
||||||
for _, i := range ii.Items() {
|
for _, i := range ii.Items() {
|
||||||
i.Pod, i.Container = n, u.opts.Container
|
i.Pod, i.Container = n, u.opts.Container
|
||||||
}
|
}
|
||||||
res, _, err := ii.Filter(u.q, false)
|
res, _, err := ii.Filter(0, u.q, false)
|
||||||
assert.Equal(t, u.err, err)
|
assert.Equal(t, u.err, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
assert.Equal(t, u.e, res)
|
assert.Equal(t, u.e, res)
|
||||||
|
|
@ -87,20 +87,20 @@ func TestLogItemsRender(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
"empty": {
|
"empty": {
|
||||||
opts: dao.LogOptions{},
|
opts: dao.LogOptions{},
|
||||||
e: "Testing 1,2,3...",
|
e: "Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
opts: dao.LogOptions{
|
opts: dao.LogOptions{
|
||||||
Container: "fred",
|
Container: "fred",
|
||||||
},
|
},
|
||||||
e: "\x1b[38;5;6mfred\x1b[0m Testing 1,2,3...",
|
e: "[teal::b]fred[-::-] Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
"pod": {
|
"pod-container": {
|
||||||
opts: dao.LogOptions{
|
opts: dao.LogOptions{
|
||||||
Path: "blee/fred",
|
Path: "blee/fred",
|
||||||
Container: "blee",
|
Container: "blee",
|
||||||
},
|
},
|
||||||
e: "\x1b[38;5;6mfred\x1b[0m:\x1b[38;5;6mblee\x1b[0m Testing 1,2,3...",
|
e: "[teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
"full": {
|
"full": {
|
||||||
opts: dao.LogOptions{
|
opts: dao.LogOptions{
|
||||||
|
|
@ -108,7 +108,7 @@ func TestLogItemsRender(t *testing.T) {
|
||||||
Container: "blee",
|
Container: "blee",
|
||||||
ShowTimestamp: true,
|
ShowTimestamp: true,
|
||||||
},
|
},
|
||||||
e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00\x1b[0m \x1b[38;5;6mfred\x1b[0m:\x1b[38;5;6mblee\x1b[0m Testing 1,2,3...",
|
e: "[gray::]2018-12-14T10:36:43.326972-07:00 [teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\n",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ func TestLogItemsRender(t *testing.T) {
|
||||||
ii.Items()[0].Pod, ii.Items()[0].Container = n, u.opts.Container
|
ii.Items()[0].Pod, ii.Items()[0].Container = n, u.opts.Container
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
res := make([][]byte, 1)
|
res := make([][]byte, 1)
|
||||||
ii.Render(u.opts.ShowTimestamp, res)
|
ii.Render(0, u.opts.ShowTimestamp, res)
|
||||||
assert.Equal(t, u.e, string(res[0]))
|
assert.Equal(t, u.e, string(res[0]))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
|
@ -12,12 +11,14 @@ import (
|
||||||
|
|
||||||
// LogOptions represents logger options.
|
// LogOptions represents logger options.
|
||||||
type LogOptions struct {
|
type LogOptions struct {
|
||||||
|
CreateDuration time.Duration
|
||||||
Path string
|
Path string
|
||||||
Container string
|
Container string
|
||||||
DefaultContainer string
|
DefaultContainer string
|
||||||
SinceTime string
|
SinceTime string
|
||||||
Lines int64
|
Lines int64
|
||||||
SinceSeconds int64
|
SinceSeconds int64
|
||||||
|
Head bool
|
||||||
Previous bool
|
Previous bool
|
||||||
SingleContainer bool
|
SingleContainer bool
|
||||||
MultiPods bool
|
MultiPods bool
|
||||||
|
|
@ -77,6 +78,18 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
|
||||||
Previous: o.Previous,
|
Previous: o.Previous,
|
||||||
TailLines: &o.Lines,
|
TailLines: &o.Lines,
|
||||||
}
|
}
|
||||||
|
if o.Head {
|
||||||
|
var maxBytes int64 = 1000
|
||||||
|
//var defaultTail int64 = -1
|
||||||
|
//var defaultSince int64
|
||||||
|
|
||||||
|
opts.Follow = false
|
||||||
|
opts.TailLines, opts.SinceSeconds, opts.SinceTime = nil, nil, nil
|
||||||
|
//opts.TailLines = &defaultTail
|
||||||
|
//opts.SinceSeconds = &defaultSince
|
||||||
|
opts.LimitBytes = &maxBytes
|
||||||
|
return &opts
|
||||||
|
}
|
||||||
if o.SinceSeconds < 0 {
|
if o.SinceSeconds < 0 {
|
||||||
return &opts
|
return &opts
|
||||||
}
|
}
|
||||||
|
|
@ -96,21 +109,6 @@ func (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {
|
||||||
return &opts
|
return &opts
|
||||||
}
|
}
|
||||||
|
|
||||||
// FixedSizeName returns a normalize fixed size pod name if possible.
|
|
||||||
func (o *LogOptions) FixedSizeName() string {
|
|
||||||
_, n := client.Namespaced(o.Path)
|
|
||||||
tokens := strings.Split(n, "-")
|
|
||||||
if len(tokens) < 3 {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
var s []string
|
|
||||||
for i := 0; i < len(tokens)-1; i++ {
|
|
||||||
s = append(s, tokens[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecorateLog add a log header to display po/co information along with the log message.
|
// DecorateLog add a log header to display po/co information along with the log message.
|
||||||
func (o *LogOptions) DecorateLog(bytes []byte) *LogItem {
|
func (o *LogOptions) DecorateLog(bytes []byte) *LogItem {
|
||||||
item := NewLogItem(bytes)
|
item := NewLogItem(bytes)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ package dao
|
||||||
// "encoding/json"
|
// "encoding/json"
|
||||||
// "errors"
|
// "errors"
|
||||||
// "fmt"
|
// "fmt"
|
||||||
// "io/ioutil"
|
// "io"
|
||||||
// "net/http"
|
// "net/http"
|
||||||
// "net/url"
|
// "net/url"
|
||||||
// "os"
|
// "os"
|
||||||
|
|
@ -193,7 +193,7 @@ package dao
|
||||||
// case http.StatusUnauthorized:
|
// case http.StatusUnauthorized:
|
||||||
// return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server")
|
// return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server")
|
||||||
// default:
|
// default:
|
||||||
// bytesOut, err := ioutil.ReadAll(delRes.Body)
|
// bytesOut, err := io.ReadAll(delRes.Body)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// return err
|
// return err
|
||||||
// }
|
// }
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TailLogs tails a given container logs.
|
// TailLogs tails a given container logs.
|
||||||
func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
|
func (p *Pod) TailLogs(ctx context.Context, out LogChan, opts *LogOptions) error {
|
||||||
log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container)
|
log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container)
|
||||||
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -198,18 +198,18 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
|
||||||
|
|
||||||
if co, ok := GetDefaultLogContainer(po.ObjectMeta, po.Spec); ok && !opts.AllContainers {
|
if co, ok := GetDefaultLogContainer(po.ObjectMeta, po.Spec); ok && !opts.AllContainers {
|
||||||
opts.DefaultContainer = co
|
opts.DefaultContainer = co
|
||||||
return tailLogs(ctx, p, c, opts)
|
return tailLogs(ctx, p, out, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.HasContainer() && !opts.AllContainers {
|
if opts.HasContainer() && !opts.AllContainers {
|
||||||
return tailLogs(ctx, p, c, opts)
|
return tailLogs(ctx, p, out, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tailed bool
|
var tailed bool
|
||||||
for _, co := range po.Spec.InitContainers {
|
for _, co := range po.Spec.InitContainers {
|
||||||
o := opts.Clone()
|
o := opts.Clone()
|
||||||
o.Container = co.Name
|
o.Container = co.Name
|
||||||
if err := tailLogs(ctx, p, c, o); err != nil {
|
if err := tailLogs(ctx, p, out, o); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tailed = true
|
tailed = true
|
||||||
|
|
@ -217,7 +217,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
|
||||||
for _, co := range po.Spec.Containers {
|
for _, co := range po.Spec.Containers {
|
||||||
o := opts.Clone()
|
o := opts.Clone()
|
||||||
o.Container = co.Name
|
o.Container = co.Name
|
||||||
if err := tailLogs(ctx, p, c, o); err != nil {
|
if err := tailLogs(ctx, p, out, o); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tailed = true
|
tailed = true
|
||||||
|
|
@ -225,7 +225,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts *LogOptions) error {
|
||||||
for _, co := range po.Spec.EphemeralContainers {
|
for _, co := range po.Spec.EphemeralContainers {
|
||||||
o := opts.Clone()
|
o := opts.Clone()
|
||||||
o.Container = co.Name
|
o.Container = co.Name
|
||||||
if err := tailLogs(ctx, p, c, o); err != nil {
|
if err := tailLogs(ctx, p, out, o); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tailed = true
|
tailed = true
|
||||||
|
|
@ -326,21 +326,22 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func tailLogs(ctx context.Context, logger Logger, c LogChan, opts *LogOptions) error {
|
func tailLogs(ctx context.Context, logger Logger, out LogChan, opts *LogOptions) error {
|
||||||
log.Debug().Msgf("Tailing logs for %#v", opts)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
req *restclient.Request
|
req *restclient.Request
|
||||||
stream io.ReadCloser
|
stream io.ReadCloser
|
||||||
)
|
)
|
||||||
|
|
||||||
|
o := opts.ToPodLogOptions()
|
||||||
|
log.Debug().Msgf("TAIL_LOGS! %#v", o)
|
||||||
done:
|
done:
|
||||||
for r := 0; r < logRetryCount; r++ {
|
for r := 0; r < logRetryCount; r++ {
|
||||||
req, err = logger.Logs(opts.Path, opts.ToPodLogOptions())
|
req, err = logger.Logs(opts.Path, o)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// This call will block if nothing is in the stream!!
|
// This call will block if nothing is in the stream!!
|
||||||
if stream, err = req.Stream(ctx); err == nil {
|
if stream, err = req.Stream(ctx); err == nil {
|
||||||
go readLogs(stream, c, opts)
|
go readLogs(ctx, stream, out, opts)
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
log.Error().Err(err).Msg("Streaming logs")
|
log.Error().Err(err).Msg("Streaming logs")
|
||||||
|
|
@ -351,6 +352,7 @@ done:
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
log.Debug().Msgf("!!!!TAIL_LOGS CANCELED!!!!")
|
||||||
err = ctx.Err()
|
err = ctx.Err()
|
||||||
break done
|
break done
|
||||||
default:
|
default:
|
||||||
|
|
@ -358,36 +360,36 @@ done:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
return err
|
||||||
log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path)
|
|
||||||
c <- opts.DecorateLog([]byte("\n" + err.Error() + "\n"))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readLogs(stream io.ReadCloser, c LogChan, opts *LogOptions) {
|
func readLogs(ctx context.Context, stream io.ReadCloser, c LogChan, opts *LogOptions) {
|
||||||
defer func() {
|
defer func() {
|
||||||
log.Debug().Msgf(">>> Closing stream %s", opts.Info())
|
log.Debug().Msgf("READ_LOGS BAILED!!!")
|
||||||
if err := stream.Close(); err != nil {
|
if err := stream.Close(); err != nil {
|
||||||
log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info())
|
log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
log.Debug().Msgf("READ_LOGS PROCESSING %#v", opts)
|
||||||
r := bufio.NewReader(stream)
|
r := bufio.NewReader(stream)
|
||||||
for {
|
for {
|
||||||
bytes, err := r.ReadBytes('\n')
|
bytes, err := r.ReadBytes('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.EOF) {
|
if errors.Is(err, io.EOF) {
|
||||||
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info())
|
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info())
|
||||||
|
// c <- ItemEOF
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info())
|
log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info())
|
||||||
c <- opts.DecorateLog([]byte(fmt.Sprintf("\nlog stream failed: %#v\n", err)))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c <- opts.DecorateLog(bytes)
|
select {
|
||||||
|
case c <- opts.DecorateLog(bytes):
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Debug().Msgf("READER CANCELED")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/port"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
@ -28,8 +29,7 @@ type PortForwarder struct {
|
||||||
stopChan, readyChan chan struct{}
|
stopChan, readyChan chan struct{}
|
||||||
active bool
|
active bool
|
||||||
path string
|
path string
|
||||||
container string
|
tunnel port.PortTunnel
|
||||||
ports []string
|
|
||||||
age time.Time
|
age time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,58 +57,56 @@ func (p *PortForwarder) SetActive(b bool) {
|
||||||
p.active = b
|
p.active = b
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ports returns the forwarded ports mappings.
|
// Port returns the port mapping.
|
||||||
func (p *PortForwarder) Ports() []string {
|
func (p *PortForwarder) Port() string {
|
||||||
return p.ports
|
return p.tunnel.PortMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerPort returns the container port.
|
||||||
|
func (p *PortForwarder) ContainerPort() string {
|
||||||
|
return p.tunnel.ContainerPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPort returns the local port.
|
||||||
|
func (p *PortForwarder) LocalPort() string {
|
||||||
|
return p.tunnel.LocalPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path returns the pod resource path.
|
// Path returns the pod resource path.
|
||||||
func (p *PortForwarder) Path() string {
|
func (p *PortForwarder) Path() string {
|
||||||
return PortForwardID(p.path, p.container)
|
return PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
// PortForwardID computes port-forward identifier.
|
// ID returns a pf id.
|
||||||
func PortForwardID(path, co string) string {
|
func (p *PortForwarder) ID() string {
|
||||||
return path + ":" + co
|
return PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container returns the target's container.
|
// Container returns the target's container.
|
||||||
func (p *PortForwarder) Container() string {
|
func (p *PortForwarder) Container() string {
|
||||||
return p.container
|
return p.tunnel.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop terminates a port forward.
|
// Stop terminates a port forward.
|
||||||
func (p *PortForwarder) Stop() {
|
func (p *PortForwarder) Stop() {
|
||||||
log.Debug().Msgf("<<< Stopping PortForward %q %v", p.path, p.ports)
|
log.Debug().Msgf("<<< Stopping PortForward %s", p.ID())
|
||||||
p.active = false
|
p.active = false
|
||||||
close(p.stopChan)
|
close(p.stopChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FQN returns the portforward unique id.
|
// FQN returns the portforward unique id.
|
||||||
func (p *PortForwarder) FQN() string {
|
func (p *PortForwarder) FQN() string {
|
||||||
return p.path + ":" + p.container
|
return p.path + ":" + p.tunnel.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasPortMapping checks if port mapping is defined for this fwd.
|
// HasPortMapping checks if port mapping is defined for this fwd.
|
||||||
func (p *PortForwarder) HasPortMapping(m string) bool {
|
func (p *PortForwarder) HasPortMapping(portMap string) bool {
|
||||||
for _, mapping := range p.ports {
|
return p.tunnel.PortMap() == portMap
|
||||||
if mapping == m {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start initiates a port forward session for a given pod and ports.
|
// Start initiates a port forward session for a given pod and ports.
|
||||||
func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) {
|
func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.PortForwarder, error) {
|
||||||
if len(tt) == 0 {
|
p.path, p.tunnel, p.age = path, tt, time.Now()
|
||||||
return nil, fmt.Errorf("no ports assigned")
|
|
||||||
}
|
|
||||||
fwds, addrs := make([]string, 0, len(tt)), make([]string, 0, len(tt))
|
|
||||||
for _, t := range tt {
|
|
||||||
fwds, addrs = append(fwds, t.PortMap()), append(addrs, t.Address)
|
|
||||||
}
|
|
||||||
p.path, p.container, p.ports, p.age = path, co, fwds, time.Now()
|
|
||||||
|
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb})
|
auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb})
|
||||||
|
|
@ -155,10 +153,10 @@ func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portfor
|
||||||
Name(n).
|
Name(n).
|
||||||
SubResource("portforward")
|
SubResource("portforward")
|
||||||
|
|
||||||
return p.forwardPorts("POST", req.URL(), addrs, fwds)
|
return p.forwardPorts("POST", req.URL(), tt.Address, tt.PortMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarder) forwardPorts(method string, url *url.URL, addrs, ports []string) (*portforward.PortForwarder, error) {
|
func (p *PortForwarder) forwardPorts(method string, url *url.URL, addr, portMap string) (*portforward.PortForwarder, error) {
|
||||||
cfg, err := p.Client().Config().RESTConfig()
|
cfg, err := p.Client().Config().RESTConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -169,12 +167,17 @@ func (p *PortForwarder) forwardPorts(method string, url *url.URL, addrs, ports [
|
||||||
}
|
}
|
||||||
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
|
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
|
||||||
|
|
||||||
return portforward.NewOnAddresses(dialer, addrs, ports, p.stopChan, p.readyChan, p.Out, p.ErrOut)
|
return portforward.NewOnAddresses(dialer, []string{addr}, []string{portMap}, p.stopChan, p.readyChan, p.Out, p.ErrOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
// PortForwardID computes port-forward identifier.
|
||||||
|
func PortForwardID(path, co, portMap string) string {
|
||||||
|
return path + "|" + co + "|" + portMap
|
||||||
|
}
|
||||||
|
|
||||||
func codec() (serializer.CodecFactory, runtime.ParameterCodec) {
|
func codec() (serializer.CodecFactory, runtime.ParameterCodec) {
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
gv := schema.GroupVersion{Group: "", Version: "v1"}
|
gv := schema.GroupVersion{Group: "", Version: "v1"}
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,7 @@ func loadRBAC(m ResourceMetas) {
|
||||||
|
|
||||||
func loadPreferred(f Factory, m ResourceMetas) error {
|
func loadPreferred(f Factory, m ResourceMetas) error {
|
||||||
if !f.Client().ConnectionOK() {
|
if !f.Client().ConnectionOK() {
|
||||||
log.Error().Msgf("PreferredRES - No API server connection")
|
log.Error().Msgf("Load cluster resources - No API server connection")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,6 +393,9 @@ func isDeprecated(gvr client.GVR) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCRDs(f Factory, m ResourceMetas) {
|
func loadCRDs(f Factory, m ResourceMetas) {
|
||||||
|
if !f.Client().ConnectionOK() {
|
||||||
|
return
|
||||||
|
}
|
||||||
const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
|
const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
|
||||||
oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
|
oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package dao
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -89,7 +89,7 @@ func TestExtractString(t *testing.T) {
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func load(t *testing.T, n string) *unstructured.Unstructured {
|
func load(t *testing.T, n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var o unstructured.Unstructured
|
var o unstructured.Unstructured
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package dao
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
|
@ -37,14 +36,16 @@ func (d *ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, erro
|
||||||
return nil, errors.New("no screendump dir found in context")
|
return nil, errors.New("no screendump dir found in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
ff, err := ioutil.ReadDir(SanitizeFilename(dir))
|
ff, err := os.ReadDir(SanitizeFilename(dir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
oo := make([]runtime.Object, len(ff))
|
oo := make([]runtime.Object, len(ff))
|
||||||
for i, f := range ff {
|
for i, f := range ff {
|
||||||
oo[i] = render.FileRes{File: f, Dir: dir}
|
if fi, err := f.Info(); err == nil {
|
||||||
|
oo[i] = render.FileRes{File: fi, Dir: dir}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return oo, nil
|
return oo, nil
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, _ := client.Namespaced(path)
|
auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", []string{client.PatchVerb})
|
||||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets", []string{client.PatchVerb})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +94,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
|
||||||
update,
|
update,
|
||||||
metav1.PatchOptions{},
|
metav1.PatchOptions{},
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -55,13 +55,7 @@ func NewClusterMeta() ClusterMeta {
|
||||||
|
|
||||||
// Deltas diffs cluster meta return true if different, false otherwise.
|
// Deltas diffs cluster meta return true if different, false otherwise.
|
||||||
func (c ClusterMeta) Deltas(n ClusterMeta) bool {
|
func (c ClusterMeta) Deltas(n ClusterMeta) bool {
|
||||||
if c.Cpu != n.Cpu {
|
if c.Cpu != n.Cpu || c.Mem != n.Mem || c.Ephemeral != n.Ephemeral {
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c.Mem != n.Mem {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c.Ephemeral != n.Ephemeral {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,6 +70,7 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool {
|
||||||
// ClusterInfo models cluster metadata.
|
// ClusterInfo models cluster metadata.
|
||||||
type ClusterInfo struct {
|
type ClusterInfo struct {
|
||||||
cluster *Cluster
|
cluster *Cluster
|
||||||
|
factory dao.Factory
|
||||||
data ClusterMeta
|
data ClusterMeta
|
||||||
version string
|
version string
|
||||||
listeners []ClusterInfoListener
|
listeners []ClusterInfoListener
|
||||||
|
|
@ -83,11 +78,12 @@ type ClusterInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClusterInfo returns a new instance.
|
// NewClusterInfo returns a new instance.
|
||||||
func NewClusterInfo(f dao.Factory, version string) *ClusterInfo {
|
func NewClusterInfo(f dao.Factory, v string) *ClusterInfo {
|
||||||
c := ClusterInfo{
|
c := ClusterInfo{
|
||||||
|
factory: f,
|
||||||
cluster: NewCluster(f),
|
cluster: NewCluster(f),
|
||||||
data: NewClusterMeta(),
|
data: NewClusterMeta(),
|
||||||
version: version,
|
version: v,
|
||||||
cache: cache.NewLRUExpireCache(cacheSize),
|
cache: cache.NewLRUExpireCache(cacheSize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,28 +112,29 @@ func (c *ClusterInfo) Reset(f dao.Factory) {
|
||||||
c.Refresh()
|
c.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh fetches latest cluster meta.
|
// Refresh fetches the latest cluster meta.
|
||||||
func (c *ClusterInfo) Refresh() {
|
func (c *ClusterInfo) Refresh() {
|
||||||
data := NewClusterMeta()
|
data := NewClusterMeta()
|
||||||
data.Context = c.cluster.ContextName()
|
if c.factory.Client().ConnectionOK() {
|
||||||
data.Cluster = c.cluster.ClusterName()
|
data.Context = c.cluster.ContextName()
|
||||||
data.User = c.cluster.UserName()
|
data.Cluster = c.cluster.ClusterName()
|
||||||
|
data.User = c.cluster.UserName()
|
||||||
|
data.K8sVer = c.cluster.Version()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout())
|
||||||
|
defer cancel()
|
||||||
|
var mx client.ClusterMetrics
|
||||||
|
if err := c.cluster.Metrics(ctx, &mx); err == nil {
|
||||||
|
data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral
|
||||||
|
} else {
|
||||||
|
log.Warn().Err(err).Msgf("Cluster metrics failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
data.K9sVer = c.version
|
data.K9sVer = c.version
|
||||||
v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev())
|
v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev())
|
||||||
data.K9sVer, data.K9sLatest = v1.String(), v2.String()
|
data.K9sVer, data.K9sLatest = v1.String(), v2.String()
|
||||||
if v1.IsCurrent(v2) {
|
if v1.IsCurrent(v2) {
|
||||||
data.K9sLatest = ""
|
data.K9sLatest = ""
|
||||||
}
|
}
|
||||||
data.K8sVer = c.cluster.Version()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout())
|
|
||||||
defer cancel()
|
|
||||||
var mx client.ClusterMetrics
|
|
||||||
if err := c.cluster.Metrics(ctx, &mx); err == nil {
|
|
||||||
data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral
|
|
||||||
} else {
|
|
||||||
log.Warn().Err(err).Msgf("Cluster metrics failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.data.Deltas(data) {
|
if c.data.Deltas(data) {
|
||||||
c.fireMetaChanged(c.data, data)
|
c.fireMetaChanged(c.data, data)
|
||||||
|
|
@ -200,7 +197,7 @@ func fetchLatestRev() (string, error) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,8 @@ func (c *CmdBuff) Add(r rune) {
|
||||||
if c.cancel != nil {
|
if c.cancel != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var ctx context.Context
|
ctx := context.Background()
|
||||||
ctx, c.cancel = context.WithTimeout(context.Background(), keyEntryDelay)
|
ctx, c.cancel = context.WithTimeout(ctx, keyEntryDelay)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
@ -118,8 +118,8 @@ func (c *CmdBuff) Delete() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx context.Context
|
ctx := context.Background()
|
||||||
ctx, c.cancel = context.WithTimeout(context.Background(), 800*time.Millisecond)
|
ctx, c.cancel = context.WithTimeout(ctx, 800*time.Millisecond)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,8 @@ func (f *Flash) SetMessage(level FlashLevel, msg string) {
|
||||||
f.setLevelMessage(LevelMessage{Level: level, Text: msg})
|
f.setLevelMessage(LevelMessage{Level: level, Text: msg})
|
||||||
f.fireFlashChanged()
|
f.fireFlashChanged()
|
||||||
|
|
||||||
var ctx context.Context
|
ctx := context.Background()
|
||||||
ctx, f.cancel = context.WithCancel(context.Background())
|
ctx, f.cancel = context.WithCancel(ctx)
|
||||||
go f.refresh(ctx)
|
go f.refresh(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,21 @@ type LogsListener interface {
|
||||||
|
|
||||||
// LogFailed indicates a log failure.
|
// LogFailed indicates a log failure.
|
||||||
LogFailed(error)
|
LogFailed(error)
|
||||||
|
|
||||||
|
// LogStop indicates logging was canceled.
|
||||||
|
LogStop()
|
||||||
|
|
||||||
|
// LogResume indicates loggings has resumed.
|
||||||
|
LogResume()
|
||||||
|
|
||||||
|
// LogCanceled indicates no more logs will come.
|
||||||
|
LogCanceled()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log represents a resource logger.
|
// Log represents a resource logger.
|
||||||
type Log struct {
|
type Log struct {
|
||||||
factory dao.Factory
|
factory dao.Factory
|
||||||
items *dao.LogItems
|
lines *dao.LogItems
|
||||||
listeners []LogsListener
|
listeners []LogsListener
|
||||||
gvr client.GVR
|
gvr client.GVR
|
||||||
logOptions *dao.LogOptions
|
logOptions *dao.LogOptions
|
||||||
|
|
@ -45,11 +54,19 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *L
|
||||||
return &Log{
|
return &Log{
|
||||||
gvr: gvr,
|
gvr: gvr,
|
||||||
logOptions: opts,
|
logOptions: opts,
|
||||||
items: dao.NewLogItems(),
|
lines: dao.NewLogItems(),
|
||||||
flushTimeout: flushTimeout,
|
flushTimeout: flushTimeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Log) GVR() client.GVR {
|
||||||
|
return l.gvr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Log) LogOptions() *dao.LogOptions {
|
||||||
|
return l.logOptions
|
||||||
|
}
|
||||||
|
|
||||||
// SinceSeconds returns since seconds option.
|
// SinceSeconds returns since seconds option.
|
||||||
func (l *Log) SinceSeconds() int64 {
|
func (l *Log) SinceSeconds() int64 {
|
||||||
l.mx.RLock()
|
l.mx.RLock()
|
||||||
|
|
@ -58,16 +75,33 @@ func (l *Log) SinceSeconds() int64 {
|
||||||
return l.logOptions.SinceSeconds
|
return l.logOptions.SinceSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHead returns log head option.
|
||||||
|
func (l *Log) IsHead() bool {
|
||||||
|
l.mx.RLock()
|
||||||
|
defer l.mx.RUnlock()
|
||||||
|
|
||||||
|
return l.logOptions.Head
|
||||||
|
}
|
||||||
|
|
||||||
// ToggleShowTimestamp toggles to logs timestamps.
|
// ToggleShowTimestamp toggles to logs timestamps.
|
||||||
func (l *Log) ToggleShowTimestamp(b bool) {
|
func (l *Log) ToggleShowTimestamp(b bool) {
|
||||||
l.logOptions.ShowTimestamp = b
|
l.logOptions.ShowTimestamp = b
|
||||||
l.Refresh()
|
l.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Log) Head(ctx context.Context, c dao.LogChan) {
|
||||||
|
l.mx.Lock()
|
||||||
|
{
|
||||||
|
l.logOptions.Head = true
|
||||||
|
}
|
||||||
|
l.mx.Unlock()
|
||||||
|
l.Restart(ctx, c, true)
|
||||||
|
}
|
||||||
|
|
||||||
// SetSinceSeconds sets the logs retrieval time.
|
// SetSinceSeconds sets the logs retrieval time.
|
||||||
func (l *Log) SetSinceSeconds(i int64) {
|
func (l *Log) SetSinceSeconds(ctx context.Context, c dao.LogChan, i int64) {
|
||||||
l.logOptions.SinceSeconds = i
|
l.logOptions.SinceSeconds, l.logOptions.Head = i, false
|
||||||
l.Restart()
|
l.Restart(ctx, c, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure sets logger configuration.
|
// Configure sets logger configuration.
|
||||||
|
|
@ -100,7 +134,7 @@ func (l *Log) Init(f dao.Factory) {
|
||||||
func (l *Log) Clear() {
|
func (l *Log) Clear() {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
{
|
{
|
||||||
l.items.Clear()
|
l.lines.Clear()
|
||||||
l.lastSent = 0
|
l.lastSent = 0
|
||||||
}
|
}
|
||||||
l.mx.Unlock()
|
l.mx.Unlock()
|
||||||
|
|
@ -111,21 +145,24 @@ func (l *Log) Clear() {
|
||||||
// Refresh refreshes the logs.
|
// Refresh refreshes the logs.
|
||||||
func (l *Log) Refresh() {
|
func (l *Log) Refresh() {
|
||||||
l.fireLogCleared()
|
l.fireLogCleared()
|
||||||
ll := make([][]byte, l.items.Len())
|
ll := make([][]byte, l.lines.Len())
|
||||||
l.items.Render(l.logOptions.ShowTimestamp, ll)
|
l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
|
||||||
l.fireLogChanged(ll)
|
l.fireLogChanged(ll)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart restarts the logger.
|
// Restart restarts the logger.
|
||||||
func (l *Log) Restart() {
|
func (l *Log) Restart(ctx context.Context, c dao.LogChan, clear bool) {
|
||||||
l.Clear()
|
|
||||||
l.Stop()
|
l.Stop()
|
||||||
l.Start()
|
if clear {
|
||||||
|
l.Clear()
|
||||||
|
}
|
||||||
|
l.fireLogResume()
|
||||||
|
l.Start(ctx, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts logging.
|
// Start starts logging.
|
||||||
func (l *Log) Start() {
|
func (l *Log) Start(ctx context.Context, c dao.LogChan) {
|
||||||
if err := l.load(); err != nil {
|
if err := l.load(ctx, c); err != nil {
|
||||||
log.Error().Err(err).Msgf("Tail logs failed!")
|
log.Error().Err(err).Msgf("Tail logs failed!")
|
||||||
l.fireLogError(err)
|
l.fireLogError(err)
|
||||||
}
|
}
|
||||||
|
|
@ -134,23 +171,20 @@ func (l *Log) Start() {
|
||||||
// Stop terminates logging.
|
// Stop terminates logging.
|
||||||
func (l *Log) Stop() {
|
func (l *Log) Stop() {
|
||||||
defer log.Debug().Msgf("<<<< Logger STOPPED!")
|
defer log.Debug().Msgf("<<<< Logger STOPPED!")
|
||||||
if l.cancelFn != nil {
|
l.cancel()
|
||||||
l.cancelFn()
|
|
||||||
l.cancelFn = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets the log lines (for testing only!)
|
// Set sets the log lines (for testing only!)
|
||||||
func (l *Log) Set(items *dao.LogItems) {
|
func (l *Log) Set(lines *dao.LogItems) {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
{
|
{
|
||||||
l.items.Merge(items)
|
l.lines.Merge(lines)
|
||||||
}
|
}
|
||||||
l.mx.Unlock()
|
l.mx.Unlock()
|
||||||
|
|
||||||
l.fireLogCleared()
|
l.fireLogCleared()
|
||||||
ll := make([][]byte, l.items.Len())
|
ll := make([][]byte, l.lines.Len())
|
||||||
l.items.Render(l.logOptions.ShowTimestamp, ll)
|
l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
|
||||||
l.fireLogChanged(ll)
|
l.fireLogChanged(ll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,34 +197,31 @@ func (l *Log) ClearFilter() {
|
||||||
l.mx.Unlock()
|
l.mx.Unlock()
|
||||||
|
|
||||||
l.fireLogCleared()
|
l.fireLogCleared()
|
||||||
ll := make([][]byte, l.items.Len())
|
ll := make([][]byte, l.lines.Len())
|
||||||
l.items.Render(l.logOptions.ShowTimestamp, ll)
|
l.lines.Render(0, l.logOptions.ShowTimestamp, ll)
|
||||||
l.fireLogChanged(ll)
|
l.fireLogChanged(ll)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter filters the model using either fuzzy or regexp.
|
// Filter filters the model using either fuzzy or regexp.
|
||||||
func (l *Log) Filter(q string) {
|
func (l *Log) Filter(q string) {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
{
|
||||||
|
l.filter = q
|
||||||
if len(q) == 0 {
|
|
||||||
l.filter = ""
|
|
||||||
l.fireLogCleared()
|
|
||||||
l.fireLogBuffChanged(l.items)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
l.mx.Unlock()
|
||||||
|
|
||||||
l.filter = q
|
|
||||||
l.fireLogCleared()
|
l.fireLogCleared()
|
||||||
l.fireLogBuffChanged(l.items)
|
l.fireLogBuffChanged(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) load() error {
|
func (l *Log) load(ctx context.Context, c dao.LogChan) error {
|
||||||
var ctx context.Context
|
if l.cancelFn != nil {
|
||||||
|
l.cancelFn()
|
||||||
|
l.cancelFn = nil
|
||||||
|
}
|
||||||
|
|
||||||
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
|
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
|
||||||
ctx, l.cancelFn = context.WithCancel(ctx)
|
ctx, l.cancelFn = context.WithCancel(ctx)
|
||||||
|
|
||||||
c := make(dao.LogChan, 10)
|
|
||||||
go l.updateLogs(ctx, c)
|
go l.updateLogs(ctx, c)
|
||||||
|
|
||||||
accessor, err := dao.AccessorFor(l.factory, l.gvr)
|
accessor, err := dao.AccessorFor(l.factory, l.gvr)
|
||||||
|
|
@ -205,40 +236,40 @@ func (l *Log) load() error {
|
||||||
go func() {
|
go func() {
|
||||||
if err = loggable.TailLogs(ctx, c, l.logOptions); err != nil {
|
if err = loggable.TailLogs(ctx, c, l.logOptions); err != nil {
|
||||||
log.Error().Err(err).Msgf("Tail logs failed")
|
log.Error().Err(err).Msgf("Tail logs failed")
|
||||||
l.mx.Lock()
|
l.cancel()
|
||||||
if l.cancelFn != nil {
|
|
||||||
l.cancelFn()
|
|
||||||
}
|
|
||||||
l.mx.Unlock()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Log) cancel() {
|
||||||
|
l.mx.Lock()
|
||||||
|
{
|
||||||
|
if l.cancelFn == nil {
|
||||||
|
l.mx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.cancelFn()
|
||||||
|
l.cancelFn = nil
|
||||||
|
}
|
||||||
|
l.mx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Append adds a log line.
|
// Append adds a log line.
|
||||||
func (l *Log) Append(line *dao.LogItem) {
|
func (l *Log) Append(line *dao.LogItem) {
|
||||||
if line == nil || line.IsEmpty() {
|
if line == nil || line.IsEmpty() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.mx.Lock()
|
|
||||||
{
|
|
||||||
l.logOptions.SinceTime = line.Timestamp
|
|
||||||
}
|
|
||||||
l.mx.Unlock()
|
|
||||||
|
|
||||||
if l.items.Len() == 0 {
|
|
||||||
l.fireLogCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
if l.items.Len() < int(l.logOptions.Lines) {
|
l.logOptions.SinceTime = line.GetTimestamp()
|
||||||
l.items.Add(line)
|
if l.lines.Len() < int(l.logOptions.Lines) {
|
||||||
|
l.lines.Add(line)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.items.Shift(line)
|
l.lines.Shift(line)
|
||||||
l.lastSent--
|
l.lastSent--
|
||||||
if l.lastSent < 0 {
|
if l.lastSent < 0 {
|
||||||
l.lastSent = 0
|
l.lastSent = 0
|
||||||
|
|
@ -250,36 +281,40 @@ func (l *Log) Notify() {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
if l.lastSent < l.items.Len() {
|
if l.lastSent < l.lines.Len() {
|
||||||
l.fireLogBuffChanged(l.items.Subset(l.lastSent))
|
l.fireLogBuffChanged(l.lastSent)
|
||||||
l.lastSent = l.items.Len()
|
l.lastSent = l.lines.Len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToggleAllContainers toggles to show all containers logs.
|
// ToggleAllContainers toggles to show all containers logs.
|
||||||
func (l *Log) ToggleAllContainers() {
|
func (l *Log) ToggleAllContainers(ctx context.Context, c dao.LogChan) {
|
||||||
l.logOptions.ToggleAllContainers()
|
l.logOptions.ToggleAllContainers()
|
||||||
l.Restart()
|
l.Restart(ctx, c, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
|
func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
|
||||||
defer func() {
|
defer log.Debug().Msgf("updateLogs view bailing out!")
|
||||||
log.Debug().Msgf("updateLogs view bailing out!")
|
|
||||||
}()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case item, ok := <-c:
|
case item, ok := <-c:
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msgf("Closed channel detected. Bailing out...")
|
log.Debug().Msgf("Closed channel detected. Bailing out!")
|
||||||
l.Append(item)
|
l.Append(item)
|
||||||
l.Notify()
|
l.Notify()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if item == dao.ItemEOF {
|
||||||
|
log.Debug().Msgf("!!!!!GOT EOF!!!!!!")
|
||||||
|
l.fireCanceled()
|
||||||
|
return
|
||||||
|
}
|
||||||
l.Append(item)
|
l.Append(item)
|
||||||
var overflow bool
|
var overflow bool
|
||||||
l.mx.RLock()
|
l.mx.RLock()
|
||||||
{
|
{
|
||||||
overflow = int64(l.items.Len()-l.lastSent) > l.logOptions.Lines
|
overflow = int64(l.lines.Len()-l.lastSent) > l.logOptions.Lines
|
||||||
}
|
}
|
||||||
l.mx.RUnlock()
|
l.mx.RUnlock()
|
||||||
if overflow {
|
if overflow {
|
||||||
|
|
@ -288,6 +323,7 @@ func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
|
||||||
case <-time.After(l.flushTimeout):
|
case <-time.After(l.flushTimeout):
|
||||||
l.Notify()
|
l.Notify()
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
log.Debug().Msgf("!!!LOG_MODEL IS CANCELED!!!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -295,11 +331,17 @@ func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
|
||||||
|
|
||||||
// AddListener adds a new model listener.
|
// AddListener adds a new model listener.
|
||||||
func (l *Log) AddListener(listener LogsListener) {
|
func (l *Log) AddListener(listener LogsListener) {
|
||||||
|
l.mx.Lock()
|
||||||
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
l.listeners = append(l.listeners, listener)
|
l.listeners = append(l.listeners, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveListener delete a listener from the list.
|
// RemoveListener delete a listener from the list.
|
||||||
func (l *Log) RemoveListener(listener LogsListener) {
|
func (l *Log) RemoveListener(listener LogsListener) {
|
||||||
|
l.mx.Lock()
|
||||||
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
victim := -1
|
victim := -1
|
||||||
for i, lis := range l.listeners {
|
for i, lis := range l.listeners {
|
||||||
if lis == listener {
|
if lis == listener {
|
||||||
|
|
@ -313,19 +355,19 @@ func (l *Log) RemoveListener(listener LogsListener) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) applyFilter(q string) ([][]byte, error) {
|
func (l *Log) applyFilter(index int, q string) ([][]byte, error) {
|
||||||
if q == "" {
|
if q == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
matches, indices, err := l.items.Filter(q, l.logOptions.ShowTimestamp)
|
matches, indices, err := l.lines.Filter(index, q, l.logOptions.ShowTimestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// No filter!
|
// No filter!
|
||||||
if matches == nil {
|
if matches == nil {
|
||||||
ll := make([][]byte, l.items.Len())
|
ll := make([][]byte, l.lines.Len())
|
||||||
l.items.Render(l.logOptions.ShowTimestamp, ll)
|
l.lines.Render(index, l.logOptions.ShowTimestamp, ll)
|
||||||
return ll, nil
|
return ll, nil
|
||||||
}
|
}
|
||||||
// Blank filter
|
// Blank filter
|
||||||
|
|
@ -333,31 +375,45 @@ func (l *Log) applyFilter(q string) ([][]byte, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
filtered := make([][]byte, 0, len(matches))
|
filtered := make([][]byte, 0, len(matches))
|
||||||
lines := l.items.Lines(l.logOptions.ShowTimestamp)
|
ll := make([][]byte, l.lines.Len())
|
||||||
|
l.lines.Lines(index, l.logOptions.ShowTimestamp, ll)
|
||||||
for i, idx := range matches {
|
for i, idx := range matches {
|
||||||
filtered = append(filtered, color.Highlight(lines[idx], indices[i], 209))
|
filtered = append(filtered, color.Highlight(ll[idx], indices[i], 209))
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered, nil
|
return filtered, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) fireLogBuffChanged(lines *dao.LogItems) {
|
func (l *Log) fireLogBuffChanged(index int) {
|
||||||
ll := make([][]byte, lines.Len())
|
ll := make([][]byte, l.lines.Len()-index)
|
||||||
if l.filter == "" {
|
if l.filter == "" {
|
||||||
lines.Render(l.logOptions.ShowTimestamp, ll)
|
l.lines.Render(index, l.logOptions.ShowTimestamp, ll)
|
||||||
} else {
|
} else {
|
||||||
ff, err := l.applyFilter(l.filter)
|
ff, err := l.applyFilter(index, l.filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.fireLogError(err)
|
l.fireLogError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ll = ff
|
ll = ff
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ll) > 0 {
|
if len(ll) > 0 {
|
||||||
l.fireLogChanged(ll)
|
l.fireLogChanged(ll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Log) fireLogResume() {
|
||||||
|
for _, lis := range l.listeners {
|
||||||
|
lis.LogResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Log) fireCanceled() {
|
||||||
|
for _, lis := range l.listeners {
|
||||||
|
lis.LogCanceled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Log) fireLogError(err error) {
|
func (l *Log) fireLogError(err error) {
|
||||||
for _, lis := range l.listeners {
|
for _, lis := range l.listeners {
|
||||||
lis.LogFailed(err)
|
lis.LogFailed(err)
|
||||||
|
|
@ -371,7 +427,13 @@ func (l *Log) fireLogChanged(lines [][]byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) fireLogCleared() {
|
func (l *Log) fireLogCleared() {
|
||||||
for _, lis := range l.listeners {
|
var ll []LogsListener
|
||||||
|
l.mx.RLock()
|
||||||
|
{
|
||||||
|
ll = l.listeners
|
||||||
|
}
|
||||||
|
l.mx.RUnlock()
|
||||||
|
for _, lis := range ll {
|
||||||
lis.LogCleared()
|
lis.LogCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,14 @@ func TestUpdateLogs(t *testing.T) {
|
||||||
v := newMockLogView()
|
v := newMockLogView()
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
c := make(dao.LogChan)
|
c := make(dao.LogChan, 2)
|
||||||
go func() {
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
m.updateLogs(context.Background(), c)
|
defer cancel()
|
||||||
}()
|
go m.updateLogs(ctx, c)
|
||||||
|
|
||||||
for i := 0; i < 2*size; i++ {
|
for i := 0; i < 2*size; i++ {
|
||||||
c <- dao.NewLogItemFromString("line" + strconv.Itoa(i))
|
c <- dao.NewLogItemFromString("line" + strconv.Itoa(i))
|
||||||
}
|
}
|
||||||
close(c)
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
assert.Equal(t, size, v.count)
|
assert.Equal(t, size, v.count)
|
||||||
|
|
@ -45,11 +44,12 @@ func BenchmarkUpdateLogs(b *testing.B) {
|
||||||
go func() {
|
go func() {
|
||||||
m.updateLogs(context.Background(), c)
|
m.updateLogs(context.Background(), c)
|
||||||
}()
|
}()
|
||||||
|
item := dao.NewLogItem([]byte("\033[0;38m2018-12-14T10:36:43.326972-07:00 \033[0;32mblee line"))
|
||||||
|
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
c <- dao.NewLogItemFromString("line" + strconv.Itoa(n))
|
c <- item
|
||||||
}
|
}
|
||||||
close(c)
|
close(c)
|
||||||
}
|
}
|
||||||
|
|
@ -75,5 +75,8 @@ func newMockLogView() *mockLogView {
|
||||||
func (t *mockLogView) LogChanged(ll [][]byte) {
|
func (t *mockLogView) LogChanged(ll [][]byte) {
|
||||||
t.count += len(ll)
|
t.count += len(ll)
|
||||||
}
|
}
|
||||||
|
func (t *mockLogView) LogStop() {}
|
||||||
|
func (t *mockLogView) LogCanceled() {}
|
||||||
|
func (t *mockLogView) LogResume() {}
|
||||||
func (t *mockLogView) LogCleared() {}
|
func (t *mockLogView) LogCleared() {}
|
||||||
func (t *mockLogView) LogFailed(err error) {}
|
func (t *mockLogView) LogFailed(err error) {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package model_test
|
package model_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -32,9 +33,8 @@ func TestLogFullBuffer(t *testing.T) {
|
||||||
m.Notify()
|
m.Notify()
|
||||||
|
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 1, v.clearCalled)
|
assert.Equal(t, 0, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
// assert.Equal(t, data.Items()[4:].Lines(false), v.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogFilter(t *testing.T) {
|
func TestLogFilter(t *testing.T) {
|
||||||
|
|
@ -79,13 +79,13 @@ func TestLogFilter(t *testing.T) {
|
||||||
|
|
||||||
m.Notify()
|
m.Notify()
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 2, v.clearCalled)
|
assert.Equal(t, 1, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
assert.Equal(t, u.e, len(v.data))
|
assert.Equal(t, u.e, len(v.data))
|
||||||
|
|
||||||
m.ClearFilter()
|
m.ClearFilter()
|
||||||
assert.Equal(t, 2, v.dataCalled)
|
assert.Equal(t, 2, v.dataCalled)
|
||||||
assert.Equal(t, 3, v.clearCalled)
|
assert.Equal(t, 2, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
assert.Equal(t, size, len(v.data))
|
assert.Equal(t, size, len(v.data))
|
||||||
})
|
})
|
||||||
|
|
@ -99,7 +99,10 @@ func TestLogStartStop(t *testing.T) {
|
||||||
v := newTestView()
|
v := newTestView()
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
m.Start()
|
c := make(dao.LogChan, 2)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
m.Start(ctx, c)
|
||||||
data := dao.NewLogItems()
|
data := dao.NewLogItems()
|
||||||
data.Add(dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2"))
|
data.Add(dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2"))
|
||||||
for _, d := range data.Items() {
|
for _, d := range data.Items() {
|
||||||
|
|
@ -109,7 +112,7 @@ func TestLogStartStop(t *testing.T) {
|
||||||
m.Stop()
|
m.Stop()
|
||||||
|
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 1, v.clearCalled)
|
assert.Equal(t, 0, v.clearCalled)
|
||||||
assert.Equal(t, 1, v.errCalled)
|
assert.Equal(t, 1, v.errCalled)
|
||||||
assert.Equal(t, 2, len(v.data))
|
assert.Equal(t, 2, len(v.data))
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +135,7 @@ func TestLogClear(t *testing.T) {
|
||||||
m.Clear()
|
m.Clear()
|
||||||
|
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 2, v.clearCalled)
|
assert.Equal(t, 1, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
assert.Equal(t, 0, len(v.data))
|
assert.Equal(t, 0, len(v.data))
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +154,9 @@ func TestLogBasic(t *testing.T) {
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 1, v.clearCalled)
|
assert.Equal(t, 1, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
assert.Equal(t, data.Lines(false), v.data)
|
ll := make([][]byte, data.Len())
|
||||||
|
data.Lines(0, false, ll)
|
||||||
|
assert.Equal(t, ll, v.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogAppend(t *testing.T) {
|
func TestLogAppend(t *testing.T) {
|
||||||
|
|
@ -163,7 +168,9 @@ func TestLogAppend(t *testing.T) {
|
||||||
items := dao.NewLogItems()
|
items := dao.NewLogItems()
|
||||||
items.Add(dao.NewLogItemFromString("blah blah"))
|
items.Add(dao.NewLogItemFromString("blah blah"))
|
||||||
m.Set(items)
|
m.Set(items)
|
||||||
assert.Equal(t, items.Lines(false), v.data)
|
ll := make([][]byte, items.Len())
|
||||||
|
items.Lines(0, false, ll)
|
||||||
|
assert.Equal(t, ll, v.data)
|
||||||
|
|
||||||
data := dao.NewLogItems()
|
data := dao.NewLogItems()
|
||||||
data.Add(
|
data.Add(
|
||||||
|
|
@ -174,7 +181,9 @@ func TestLogAppend(t *testing.T) {
|
||||||
m.Append(d)
|
m.Append(d)
|
||||||
}
|
}
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, items.Lines(false), v.data)
|
ll = make([][]byte, items.Len())
|
||||||
|
items.Lines(0, false, ll)
|
||||||
|
assert.Equal(t, ll, v.data)
|
||||||
|
|
||||||
m.Notify()
|
m.Notify()
|
||||||
assert.Equal(t, 2, v.dataCalled)
|
assert.Equal(t, 2, v.dataCalled)
|
||||||
|
|
@ -203,7 +212,7 @@ func TestLogTimedout(t *testing.T) {
|
||||||
}
|
}
|
||||||
m.Notify()
|
m.Notify()
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 2, v.clearCalled)
|
assert.Equal(t, 1, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m"
|
const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m"
|
||||||
assert.Equal(t, e, string(v.data[0]))
|
assert.Equal(t, e, string(v.data[0]))
|
||||||
|
|
@ -215,9 +224,13 @@ func TestToggleAllContainers(t *testing.T) {
|
||||||
m := model.NewLog(client.NewGVR(""), opts, 10*time.Millisecond)
|
m := model.NewLog(client.NewGVR(""), opts, 10*time.Millisecond)
|
||||||
m.Init(makeFactory())
|
m.Init(makeFactory())
|
||||||
assert.Equal(t, "blee", m.GetContainer())
|
assert.Equal(t, "blee", m.GetContainer())
|
||||||
m.ToggleAllContainers()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := make(dao.LogChan, 2)
|
||||||
|
m.ToggleAllContainers(ctx, c)
|
||||||
assert.Equal(t, "", m.GetContainer())
|
assert.Equal(t, "", m.GetContainer())
|
||||||
m.ToggleAllContainers()
|
m.ToggleAllContainers(ctx, c)
|
||||||
assert.Equal(t, "blee", m.GetContainer())
|
assert.Equal(t, "blee", m.GetContainer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,16 +258,17 @@ func newTestView() *testView {
|
||||||
return &testView{}
|
return &testView{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *testView) LogCanceled() {}
|
||||||
|
func (t *testView) LogStop() {}
|
||||||
|
func (t *testView) LogResume() {}
|
||||||
func (t *testView) LogChanged(ll [][]byte) {
|
func (t *testView) LogChanged(ll [][]byte) {
|
||||||
t.data = ll
|
t.data = ll
|
||||||
t.dataCalled++
|
t.dataCalled++
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testView) LogCleared() {
|
func (t *testView) LogCleared() {
|
||||||
t.clearCalled++
|
t.clearCalled++
|
||||||
t.data = nil
|
t.data = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testView) LogFailed(err error) {
|
func (t *testView) LogFailed(err error) {
|
||||||
fmt.Println("LogErr", err)
|
fmt.Println("LogErr", err)
|
||||||
t.errCalled++
|
t.errCalled++
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ func (t *Table) Peek() render.TableData {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) updater(ctx context.Context) {
|
func (t *Table) updater(ctx context.Context) {
|
||||||
defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr)
|
defer log.Debug().Msgf("TABLE-UPDATER canceled -- %q", t.gvr)
|
||||||
|
|
||||||
bf := backoff.NewExponentialBackOff()
|
bf := backoff.NewExponentialBackOff()
|
||||||
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval
|
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
|
|
@ -139,7 +139,7 @@ func TestTableGenericHydrate(t *testing.T) {
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func mustLoad(n string) *unstructured.Unstructured {
|
func mustLoad(n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +151,7 @@ func mustLoad(n string) *unstructured.Unstructured {
|
||||||
}
|
}
|
||||||
|
|
||||||
func load(t *testing.T, n string) *unstructured.Unstructured {
|
func load(t *testing.T, n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
var o unstructured.Unstructured
|
var o unstructured.Unstructured
|
||||||
err = json.Unmarshal(raw, &o)
|
err = json.Unmarshal(raw, &o)
|
||||||
|
|
@ -160,7 +160,7 @@ func load(t *testing.T, n string) *unstructured.Unstructured {
|
||||||
}
|
}
|
||||||
|
|
||||||
func raw(t *testing.T, n string) []byte {
|
func raw(t *testing.T, n string) []byte {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
|
|
@ -122,7 +122,7 @@ func makeTableFactory() tableFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustLoad(n string) *unstructured.Unstructured {
|
func mustLoad(n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -138,7 +137,7 @@ func (b *Benchmark) save(cluster string, r io.Reader) error {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
bb, err := ioutil.ReadAll(r)
|
bb, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -97,7 +96,7 @@ func (Benchmark) diagnose(ns string, ff Fields) error {
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func (Benchmark) readFile(file string) (string, error) {
|
func (Benchmark) readFile(file string) (string, error) {
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
@ -38,7 +38,7 @@ func TestAugmentRow(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
data, err := ioutil.ReadFile(u.file)
|
data, err := os.ReadFile(u.file)
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
fields := make(Fields, 8)
|
fields := make(Fields, 8)
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,10 @@ func (Dir) Render(o interface{}, ns string, r *Row) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
name := "🦄 "
|
name := "🦄 "
|
||||||
if d.Info.IsDir() {
|
if d.Entry.IsDir() {
|
||||||
name = "📁 "
|
name = "📁 "
|
||||||
}
|
}
|
||||||
name += d.Info.Name()
|
name += d.Entry.Name()
|
||||||
r.ID, r.Fields = d.Path, append(r.Fields, name)
|
r.ID, r.Fields = d.Path, append(r.Fields, name)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -49,8 +49,8 @@ func (Dir) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
|
||||||
// DirRes represents an alias resource.
|
// DirRes represents an alias resource.
|
||||||
type DirRes struct {
|
type DirRes struct {
|
||||||
Info os.FileInfo
|
Entry os.DirEntry
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectKind returns a schema object.
|
// GetObjectKind returns a schema object.
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ func TestPortForwardRender(t *testing.T) {
|
||||||
"blee",
|
"blee",
|
||||||
"fred",
|
"fred",
|
||||||
"co",
|
"co",
|
||||||
"p1",
|
"p1:p2",
|
||||||
"http://0.0.0.0:p1/",
|
"http://0.0.0.0:p1/",
|
||||||
"1",
|
"1",
|
||||||
"1",
|
"1",
|
||||||
|
|
@ -47,8 +47,8 @@ func (f fwd) Container() string {
|
||||||
return "co"
|
return "co"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fwd) Ports() []string {
|
func (f fwd) Port() string {
|
||||||
return []string{"p1"}
|
return "p1:p2"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fwd) Active() bool {
|
func (f fwd) Active() bool {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ type Forwarder interface {
|
||||||
Container() string
|
Container() string
|
||||||
|
|
||||||
// Ports returns container exposed ports.
|
// Ports returns container exposed ports.
|
||||||
Ports() []string
|
Port() string
|
||||||
|
|
||||||
// Active returns forwarder current state.
|
// Active returns forwarder current state.
|
||||||
Active() bool
|
Active() bool
|
||||||
|
|
@ -60,7 +60,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
|
||||||
return fmt.Errorf("expecting a ForwardRes but got %T", o)
|
return fmt.Errorf("expecting a ForwardRes but got %T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
ports := strings.Split(pf.Ports()[0], ":")
|
ports := strings.Split(pf.Port(), ":")
|
||||||
ns, n := client.Namespaced(pf.Path())
|
ns, n := client.Namespaced(pf.Path())
|
||||||
|
|
||||||
r.ID = pf.Path()
|
r.ID = pf.Path()
|
||||||
|
|
@ -68,7 +68,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
|
||||||
ns,
|
ns,
|
||||||
trimContainer(n),
|
trimContainer(n),
|
||||||
pf.Container(),
|
pf.Container(),
|
||||||
strings.Join(pf.Ports(), ","),
|
pf.Port(),
|
||||||
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
|
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
|
||||||
AsThousands(int64(pf.Config.C)),
|
AsThousands(int64(pf.Config.C)),
|
||||||
AsThousands(int64(pf.Config.N)),
|
AsThousands(int64(pf.Config.N)),
|
||||||
|
|
@ -82,11 +82,13 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func trimContainer(n string) string {
|
func trimContainer(n string) string {
|
||||||
tokens := strings.Split(n, ":")
|
tokens := strings.Split(n, "|")
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
return tokens[0]
|
_, name := client.Namespaced(tokens[0])
|
||||||
|
|
||||||
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlFor computes fq url for a given benchmark configuration.
|
// UrlFor computes fq url for a given benchmark configuration.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package render_test
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func load(t testing.TB, n string) *unstructured.Unstructured {
|
func load(t testing.TB, n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var o unstructured.Unstructured
|
var o unstructured.Unstructured
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ func NewApp(cfg *config.Config, context string) *App {
|
||||||
a.views = map[string]tview.Primitive{
|
a.views = map[string]tview.Primitive{
|
||||||
"menu": NewMenu(a.Styles),
|
"menu": NewMenu(a.Styles),
|
||||||
"logo": NewLogo(a.Styles),
|
"logo": NewLogo(a.Styles),
|
||||||
"prompt": NewPrompt(a.Config.K9s.NoIcons, a.Styles),
|
"prompt": NewPrompt(&a, a.Config.K9s.NoIcons, a.Styles),
|
||||||
"crumbs": NewCrumbs(a.Styles),
|
"crumbs": NewCrumbs(a.Styles),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +60,9 @@ func (a *App) Init() {
|
||||||
|
|
||||||
// QueueUpdate queues up a ui action.
|
// QueueUpdate queues up a ui action.
|
||||||
func (a *App) QueueUpdate(f func()) {
|
func (a *App) QueueUpdate(f func()) {
|
||||||
|
if a.Application == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
a.Application.QueueUpdate(f)
|
a.Application.QueueUpdate(f)
|
||||||
}()
|
}()
|
||||||
|
|
@ -67,6 +70,9 @@ func (a *App) QueueUpdate(f func()) {
|
||||||
|
|
||||||
// QueueUpdateDraw queues up a ui action and redraw the ui.
|
// QueueUpdateDraw queues up a ui action and redraw the ui.
|
||||||
func (a *App) QueueUpdateDraw(f func()) {
|
func (a *App) QueueUpdateDraw(f func()) {
|
||||||
|
if a.Application == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
a.Application.QueueUpdateDraw(f)
|
a.Application.QueueUpdateDraw(f)
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e
|
||||||
log.Warn().Err(err).Msg("CustomView watcher failed")
|
log.Warn().Err(err).Msg("CustomView watcher failed")
|
||||||
return
|
return
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Debug().Msgf("CustomViewWatcher Done `%s!!", config.K9sViewConfigFile)
|
log.Debug().Msgf("CustomViewWatcher CANCELED `%s!!", config.K9sViewConfigFile)
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
log.Error().Err(err).Msg("Closing CustomView watcher")
|
log.Error().Err(err).Msg("Closing CustomView watcher")
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error
|
||||||
log.Info().Err(err).Msg("Skin watcher failed")
|
log.Info().Err(err).Msg("Skin watcher failed")
|
||||||
return
|
return
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile)
|
log.Debug().Msgf("SkinWatcher CANCELED `%s!!", c.skinFile)
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
log.Error().Err(err).Msg("Closing Skin watcher")
|
log.Error().Err(err).Msg("Closing Skin watcher")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ func ShowDelete(styles config.Dialog, pages *ui.Pages, msg string, ok okFunc, ca
|
||||||
SetButtonTextColor(styles.ButtonFgColor.Color()).
|
SetButtonTextColor(styles.ButtonFgColor.Color()).
|
||||||
SetLabelColor(styles.LabelFgColor.Color()).
|
SetLabelColor(styles.LabelFgColor.Color()).
|
||||||
SetFieldTextColor(styles.FieldFgColor.Color())
|
SetFieldTextColor(styles.FieldFgColor.Color())
|
||||||
f.AddCheckbox("Cascade:", cascade, func(checked bool) {
|
f.AddCheckbox("Cascade:", cascade, func(_ string, checked bool) {
|
||||||
cascade = checked
|
cascade = checked
|
||||||
})
|
})
|
||||||
f.AddCheckbox("Force:", force, func(checked bool) {
|
f.AddCheckbox("Force:", force, func(_ string, checked bool) {
|
||||||
force = checked
|
force = checked
|
||||||
})
|
})
|
||||||
f.AddButton("Cancel", func() {
|
f.AddButton("Cancel", func() {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ func (f *Flash) StylesChanged(s *config.Styles) {
|
||||||
|
|
||||||
// Watch watches for flash changes.
|
// Watch watches for flash changes.
|
||||||
func (f *Flash) Watch(ctx context.Context, c model.FlashChan) {
|
func (f *Flash) Watch(ctx context.Context, c model.FlashChan) {
|
||||||
defer log.Debug().Msgf("Flash Canceled!")
|
defer log.Debug().Msgf("Flash Watch Canceled!")
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ type PromptModel interface {
|
||||||
type Prompt struct {
|
type Prompt struct {
|
||||||
*tview.TextView
|
*tview.TextView
|
||||||
|
|
||||||
|
app *App
|
||||||
noIcons bool
|
noIcons bool
|
||||||
icon rune
|
icon rune
|
||||||
styles *config.Styles
|
styles *config.Styles
|
||||||
|
|
@ -79,8 +80,9 @@ type Prompt struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPrompt returns a new command view.
|
// NewPrompt returns a new command view.
|
||||||
func NewPrompt(noIcons bool, styles *config.Styles) *Prompt {
|
func NewPrompt(app *App, noIcons bool, styles *config.Styles) *Prompt {
|
||||||
p := Prompt{
|
p := Prompt{
|
||||||
|
app: app,
|
||||||
styles: styles,
|
styles: styles,
|
||||||
noIcons: noIcons,
|
noIcons: noIcons,
|
||||||
TextView: tview.NewTextView(),
|
TextView: tview.NewTextView(),
|
||||||
|
|
@ -183,8 +185,15 @@ func (p *Prompt) activate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Prompt) update(s string) {
|
func (p *Prompt) update(s string) {
|
||||||
p.Clear()
|
f := func() {
|
||||||
p.write(s, "")
|
p.Clear()
|
||||||
|
p.write(s, "")
|
||||||
|
}
|
||||||
|
if p.app == nil {
|
||||||
|
f()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.app.QueueUpdate(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Prompt) suggest(text, suggestion string) {
|
func (p *Prompt) suggest(text, suggestion string) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCmdNew(t *testing.T) {
|
func TestCmdNew(t *testing.T) {
|
||||||
v := ui.NewPrompt(true, config.NewStyles())
|
v := ui.NewPrompt(nil, true, config.NewStyles())
|
||||||
model := model.NewFishBuff(':', model.CommandBuffer)
|
model := model.NewFishBuff(':', model.CommandBuffer)
|
||||||
v.SetModel(model)
|
v.SetModel(model)
|
||||||
model.AddListener(v)
|
model.AddListener(v)
|
||||||
|
|
@ -23,7 +23,7 @@ func TestCmdNew(t *testing.T) {
|
||||||
|
|
||||||
func TestCmdUpdate(t *testing.T) {
|
func TestCmdUpdate(t *testing.T) {
|
||||||
model := model.NewFishBuff(':', model.CommandBuffer)
|
model := model.NewFishBuff(':', model.CommandBuffer)
|
||||||
v := ui.NewPrompt(true, config.NewStyles())
|
v := ui.NewPrompt(nil, true, config.NewStyles())
|
||||||
v.SetModel(model)
|
v.SetModel(model)
|
||||||
|
|
||||||
model.AddListener(v)
|
model.AddListener(v)
|
||||||
|
|
@ -36,7 +36,7 @@ func TestCmdUpdate(t *testing.T) {
|
||||||
|
|
||||||
func TestCmdMode(t *testing.T) {
|
func TestCmdMode(t *testing.T) {
|
||||||
model := model.NewFishBuff(':', model.CommandBuffer)
|
model := model.NewFishBuff(':', model.CommandBuffer)
|
||||||
v := ui.NewPrompt(true, config.NewStyles())
|
v := ui.NewPrompt(&ui.App{}, true, config.NewStyles())
|
||||||
v.SetModel(model)
|
v.SetModel(model)
|
||||||
model.AddListener(v)
|
model.AddListener(v)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler {
|
||||||
clear: true,
|
clear: true,
|
||||||
binary: p.Command,
|
binary: p.Command,
|
||||||
background: p.Background,
|
background: p.Background,
|
||||||
|
pipes: p.Pipes,
|
||||||
args: args,
|
args: args,
|
||||||
}
|
}
|
||||||
if run(r.App(), opts) {
|
if run(r.App(), opts) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
@ -107,8 +108,10 @@ func (a *App) Init(version string, rate int) error {
|
||||||
a.clusterModel = model.NewClusterInfo(a.factory, a.version)
|
a.clusterModel = model.NewClusterInfo(a.factory, a.version)
|
||||||
a.clusterModel.AddListener(a.clusterInfo())
|
a.clusterModel.AddListener(a.clusterInfo())
|
||||||
a.clusterModel.AddListener(a.statusIndicator())
|
a.clusterModel.AddListener(a.statusIndicator())
|
||||||
a.clusterModel.Refresh()
|
if a.Conn().ConnectionOK() {
|
||||||
a.clusterInfo().Init()
|
a.clusterModel.Refresh()
|
||||||
|
a.clusterInfo().Init()
|
||||||
|
}
|
||||||
|
|
||||||
a.command = NewCommand(a)
|
a.command = NewCommand(a)
|
||||||
if err := a.command.Init(); err != nil {
|
if err := a.command.Init(); err != nil {
|
||||||
|
|
@ -185,6 +188,7 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
func (a *App) bindKeys() {
|
func (a *App) bindKeys() {
|
||||||
a.AddActions(ui.KeyActions{
|
a.AddActions(ui.KeyActions{
|
||||||
|
ui.KeyShiftG: ui.NewSharedKeyAction("DumpGOR", a.dumpGOR, false),
|
||||||
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
|
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
|
||||||
tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false),
|
tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false),
|
||||||
ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false),
|
ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false),
|
||||||
|
|
@ -193,6 +197,13 @@ func (a *App) bindKeys() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
bb := make([]byte, 5_000_000)
|
||||||
|
runtime.Stack(bb, true)
|
||||||
|
log.Debug().Msgf("GOR\n%s", string(bb))
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
// ActiveView returns the currently active view.
|
// ActiveView returns the currently active view.
|
||||||
func (a *App) ActiveView() model.Component {
|
func (a *App) ActiveView() model.Component {
|
||||||
return a.Content.GetPrimitive("main").(model.Component)
|
return a.Content.GetPrimitive("main").(model.Component)
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,5 @@ func TestAppNew(t *testing.T) {
|
||||||
a := view.NewApp(config.NewConfig(ks{}))
|
a := view.NewApp(config.NewConfig(ks{}))
|
||||||
a.Init("blee", 10)
|
a.Init("blee", 10)
|
||||||
|
|
||||||
assert.Equal(t, 10, len(a.GetActions()))
|
assert.Equal(t, 11, len(a.GetActions()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ func benchDir(cfg *config.Config) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBenchFile(cfg *config.Config, n string) (string, error) {
|
func readBenchFile(cfg *config.Config, n string) (string, error) {
|
||||||
data, err := ioutil.ReadFile(filepath.Join(benchDir(cfg), n))
|
data, err := os.ReadFile(filepath.Join(benchDir(cfg), n))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
|
|
@ -31,6 +32,7 @@ type Browser struct {
|
||||||
accessor dao.Accessor
|
accessor dao.Accessor
|
||||||
contextFn ContextFunc
|
contextFn ContextFunc
|
||||||
cancelFn context.CancelFunc
|
cancelFn context.CancelFunc
|
||||||
|
mx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBrowser returns a new browser.
|
// NewBrowser returns a new browser.
|
||||||
|
|
@ -140,10 +142,14 @@ func (b *Browser) Start() {
|
||||||
|
|
||||||
// Stop terminates browser updates.
|
// Stop terminates browser updates.
|
||||||
func (b *Browser) Stop() {
|
func (b *Browser) Stop() {
|
||||||
if b.cancelFn != nil {
|
b.mx.Lock()
|
||||||
b.cancelFn()
|
{
|
||||||
b.cancelFn = nil
|
if b.cancelFn != nil {
|
||||||
|
b.cancelFn()
|
||||||
|
b.cancelFn = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
b.mx.Unlock()
|
||||||
b.GetModel().RemoveListener(b)
|
b.GetModel().RemoveListener(b)
|
||||||
b.CmdBuff().RemoveListener(b)
|
b.CmdBuff().RemoveListener(b)
|
||||||
b.Table.Stop()
|
b.Table.Stop()
|
||||||
|
|
@ -213,7 +219,12 @@ func (b *Browser) Aliases() []string {
|
||||||
|
|
||||||
// TableDataChanged notifies view new data is available.
|
// TableDataChanged notifies view new data is available.
|
||||||
func (b *Browser) TableDataChanged(data render.TableData) {
|
func (b *Browser) TableDataChanged(data render.TableData) {
|
||||||
if !b.app.ConOK() || b.cancelFn == nil || !b.app.IsRunning() {
|
var cancel context.CancelFunc
|
||||||
|
b.mx.RLock()
|
||||||
|
cancel = b.cancelFn
|
||||||
|
b.mx.RUnlock()
|
||||||
|
|
||||||
|
if !b.app.ConOK() || cancel == nil || !b.app.IsRunning() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
|
@ -102,7 +103,7 @@ func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
|
||||||
c.app.QueueUpdateDraw(func() {
|
c.app.QueueUpdateDraw(func() {
|
||||||
c.Clear()
|
c.Clear()
|
||||||
c.layout()
|
c.layout()
|
||||||
row := c.setCell(0, curr.Context)
|
row := c.setCell(0, fmt.Sprintf("%s [%d]", curr.Context, runtime.NumGoroutine()))
|
||||||
row = c.setCell(row, curr.Cluster)
|
row = c.setCell(row, curr.Cluster)
|
||||||
row = c.setCell(row, curr.User)
|
row = c.setCell(row, curr.User)
|
||||||
if curr.K9sLatest != "" {
|
if curr.K9sLatest != "" {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
|
"github.com/derailed/k9s/internal/port"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
|
@ -178,89 +178,65 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ports, ok := c.isForwardable(path)
|
ports, ann, ok := c.listForwardable(path)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ShowPortForwards(c, c.GetTable().Path, ports, "", startFwdCB)
|
ShowPortForwards(c, c.GetTable().Path, ports, ann, startFwdCB)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) isForwardable(path string) ([]string, bool) {
|
func checkRunningStatus(co string, ss []v1.ContainerStatus) error {
|
||||||
po, err := fetchPod(c.App().factory, c.GetTable().Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
var co *v1.Container
|
|
||||||
cc := po.Spec.Containers
|
|
||||||
for i := range cc {
|
|
||||||
if cc[i].Name == path {
|
|
||||||
co = &cc[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if co == nil {
|
|
||||||
log.Error().Err(fmt.Errorf("unable to locate container named %q", path))
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
var cs *v1.ContainerStatus
|
var cs *v1.ContainerStatus
|
||||||
ss := po.Status.ContainerStatuses
|
|
||||||
for i := range ss {
|
for i := range ss {
|
||||||
if ss[i].Name == path {
|
if ss[i].Name == co {
|
||||||
cs = &ss[i]
|
cs = &ss[i]
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cs == nil {
|
if cs == nil {
|
||||||
log.Error().Err(fmt.Errorf("unable to locate container status for %q", path))
|
return fmt.Errorf("unable to locate container status for %q", co)
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if render.ToContainerState(cs.State) != "Running" {
|
if render.ToContainerState(cs.State) != "Running" {
|
||||||
c.App().Flash().Err(fmt.Errorf("Container %s is not running?", path))
|
return fmt.Errorf("Container %s is not running?", co)
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
portC := render.ToContainerPorts(co.Ports)
|
return nil
|
||||||
ports := strings.Split(portC, ",")
|
}
|
||||||
if len(ports) == 0 {
|
|
||||||
|
func locateContainer(co string, cc []v1.Container) (*v1.Container, error) {
|
||||||
|
for i := range cc {
|
||||||
|
if cc[i].Name == co {
|
||||||
|
return &cc[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to locate container named %q", co)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) listForwardable(path string) (port.ContainerPortSpecs, map[string]string, bool) {
|
||||||
|
po, err := fetchPod(c.App().factory, c.GetTable().Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
co, err := locateContainer(path, po.Spec.Containers)
|
||||||
|
if err != nil {
|
||||||
|
c.App().Flash().Err(err)
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkRunningStatus(path, po.Status.ContainerStatuses); err != nil {
|
||||||
|
c.App().Flash().Err(err)
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
exposedPorts := port.FromContainerPorts(path, co.Ports)
|
||||||
|
if len(exposedPorts) == 0 {
|
||||||
c.App().Flash().Err(errors.New("Container exposes no ports"))
|
c.App().Flash().Err(errors.New("Container exposes no ports"))
|
||||||
return nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
pp := make([]string, 0, len(ports))
|
return port.FromContainerPorts(path, co.Ports), po.Annotations, true
|
||||||
container, port, ok := parsePFAnn(po.Annotations[AnnDefaultPF])
|
|
||||||
if ok && container == path {
|
|
||||||
if index := indexOfPort(ports, port); index != -1 {
|
|
||||||
pp = append(pp, path+"/"+port)
|
|
||||||
ports = append(ports[:index], ports[index+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range ports {
|
|
||||||
if !isTCPPort(p) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pp = append(pp, path+"/"+p)
|
|
||||||
}
|
|
||||||
if len(pp) == 0 {
|
|
||||||
c.App().Flash().Err(errors.New("No TCP port available on container"))
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return pp, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexOfPort(pp []string, port string) int {
|
|
||||||
for i, p := range pp {
|
|
||||||
tokens := strings.Split(p, ":")
|
|
||||||
if len(tokens) == 2 {
|
|
||||||
if tokens[0] == port || tokens[1] == port {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ func (d *Dir) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
yaml, err := ioutil.ReadFile(sel)
|
yaml, err := os.ReadFile(sel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.App().Flash().Err(err)
|
d.App().Flash().Err(err)
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -157,7 +157,7 @@ func isKustomized(sel string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ff, err := ioutil.ReadDir(sel)
|
ff, err := os.ReadDir(sel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ func containsDir(sel string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ff, err := ioutil.ReadDir(sel)
|
ff, err := os.ReadDir(sel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,13 @@ func ShowDrain(view ResourceViewer, path string, defaults dao.DrainOptions, okFn
|
||||||
view.App().Flash().Clear()
|
view.App().Flash().Clear()
|
||||||
opts.Timeout = a
|
opts.Timeout = a
|
||||||
})
|
})
|
||||||
f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(v bool) {
|
f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(_ string, v bool) {
|
||||||
opts.IgnoreAllDaemonSets = v
|
opts.IgnoreAllDaemonSets = v
|
||||||
})
|
})
|
||||||
f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(v bool) {
|
f.AddCheckbox("Delete Local Data:", defaults.DeleteEmptyDirData, func(_ string, v bool) {
|
||||||
opts.DeleteEmptyDirData = v
|
opts.DeleteEmptyDirData = v
|
||||||
})
|
})
|
||||||
f.AddCheckbox("Force:", defaults.Force, func(v bool) {
|
f.AddCheckbox("Force:", defaults.Force, func(_ string, v bool) {
|
||||||
opts.Force = v
|
opts.Force = v
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
@ -32,6 +33,7 @@ const (
|
||||||
|
|
||||||
type shellOpts struct {
|
type shellOpts struct {
|
||||||
clear, background bool
|
clear, background bool
|
||||||
|
pipes []string
|
||||||
binary string
|
binary string
|
||||||
banner string
|
banner string
|
||||||
args []string
|
args []string
|
||||||
|
|
@ -96,40 +98,41 @@ func execute(opts shellOpts) error {
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer func() {
|
defer func() {
|
||||||
cancel()
|
if !opts.background {
|
||||||
clearScreen()
|
cancel()
|
||||||
|
clearScreen()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
go func() {
|
go func(cancel context.CancelFunc) {
|
||||||
|
defer log.Debug().Msgf("SIGNAL_GOR - BAILED!!")
|
||||||
select {
|
select {
|
||||||
case <-sigChan:
|
case <-sigChan:
|
||||||
log.Debug().Msg("Command canceled with signal!")
|
log.Debug().Msgf("Command canceled with signal!")
|
||||||
cancel()
|
cancel()
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
log.Debug().Msgf("SIGNAL Context CANCELED!")
|
||||||
}
|
}
|
||||||
}()
|
}(cancel)
|
||||||
|
|
||||||
log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " "))
|
cmds := make([]*exec.Cmd, 0, 1)
|
||||||
cmd := exec.Command(opts.binary, opts.args...)
|
cmd := exec.CommandContext(ctx, opts.binary, opts.args...)
|
||||||
|
log.Debug().Msgf("RUNNING> %s", cmd)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
var err error
|
for _, p := range opts.pipes {
|
||||||
if opts.background {
|
tokens := strings.Split(p, " ")
|
||||||
err = cmd.Start()
|
if len(tokens) < 2 {
|
||||||
} else {
|
continue
|
||||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
}
|
||||||
_, _ = cmd.Stdout.Write([]byte(opts.banner))
|
cmd := exec.CommandContext(ctx, tokens[0], tokens[1:]...)
|
||||||
err = cmd.Run()
|
log.Debug().Msgf("\t| %s", cmd)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
return pipe(ctx, opts, cmds...)
|
||||||
case <-ctx.Done():
|
|
||||||
return errors.New("canceled by operator")
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKu(a *App, opts shellOpts) (string, error) {
|
func runKu(a *App, opts shellOpts) (string, error) {
|
||||||
|
|
@ -358,3 +361,58 @@ func asResource(r config.Limits) v1.ResourceRequirements {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pipe(ctx context.Context, opts shellOpts, cmds ...*exec.Cmd) error {
|
||||||
|
if len(cmds) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmds) == 1 {
|
||||||
|
cmd := cmds[0]
|
||||||
|
if opts.background {
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, log.Logger, log.Logger
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
//cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
//// //Setpgid: true,
|
||||||
|
//// //Setctty: true,
|
||||||
|
// Foreground: true,
|
||||||
|
//}
|
||||||
|
_, _ = cmd.Stdout.Write([]byte(opts.banner))
|
||||||
|
|
||||||
|
log.Debug().Msgf("Running Start")
|
||||||
|
err := cmd.Run()
|
||||||
|
log.Debug().Msgf("Running Done")
|
||||||
|
return err
|
||||||
|
// select {
|
||||||
|
// case <-ctx.Done():
|
||||||
|
// return errors.New("canceled by operator")
|
||||||
|
// default:
|
||||||
|
// log.Debug().Msgf("PIPE RETURN %s", err)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
last := len(cmds) - 1
|
||||||
|
for i := 0; i < len(cmds); i++ {
|
||||||
|
cmds[i].Stderr = os.Stderr
|
||||||
|
if i+1 < len(cmds) {
|
||||||
|
r, w := io.Pipe()
|
||||||
|
cmds[i].Stdout, cmds[i+1].Stdin = w, r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmds[last].Stdout = os.Stdout
|
||||||
|
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
log.Debug().Msgf("Starting CMD %s", cmd)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("WAITING!!!")
|
||||||
|
err := cmds[len(cmds)-1].Wait()
|
||||||
|
log.Debug().Msgf("DONE WAITING!!!")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ func extractApp(ctx context.Context) (*App, error) {
|
||||||
// AsKey maps a string representation of a key to a tcell key.
|
// AsKey maps a string representation of a key to a tcell key.
|
||||||
func asKey(key string) (tcell.Key, error) {
|
func asKey(key string) (tcell.Key, error) {
|
||||||
for k, v := range tcell.KeyNames {
|
for k, v := range tcell.KeyNames {
|
||||||
if v == key {
|
if key == v {
|
||||||
return k, nil
|
return k, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,14 @@ func TestParsePFAnn(t *testing.T) {
|
||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
"named-port": {
|
"named-port": {
|
||||||
ann: "fred:blee",
|
ann: "c1:blee",
|
||||||
co: "fred",
|
co: "c1",
|
||||||
port: "blee",
|
port: "blee",
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
"port-num": {
|
"port-num": {
|
||||||
ann: "fred:1234",
|
ann: "c1:1234",
|
||||||
co: "fred",
|
co: "c1",
|
||||||
port: "1234",
|
port: "1234",
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/atotto/clipboard"
|
"github.com/atotto/clipboard"
|
||||||
|
|
@ -25,7 +24,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
logTitle = "logs"
|
logTitle = "logs"
|
||||||
logMessage = "Waiting for logs..."
|
logMessage = "Waiting for logs...\n"
|
||||||
logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
|
logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
|
||||||
logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
|
logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
|
||||||
flushTimeout = 1 * time.Millisecond
|
flushTimeout = 1 * time.Millisecond
|
||||||
|
|
@ -35,11 +34,16 @@ const (
|
||||||
type Log struct {
|
type Log struct {
|
||||||
*tview.Flex
|
*tview.Flex
|
||||||
|
|
||||||
app *App
|
app *App
|
||||||
logs *Logger
|
logs *Logger
|
||||||
indicator *LogIndicator
|
indicator *LogIndicator
|
||||||
ansiWriter io.Writer
|
ansiWriter io.Writer
|
||||||
model *model.Log
|
model *model.Log
|
||||||
|
cancelFn context.CancelFunc
|
||||||
|
cancelUpdates bool
|
||||||
|
mx sync.Mutex
|
||||||
|
logChan dao.LogChan
|
||||||
|
follow bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ model.Component = (*Log)(nil)
|
var _ model.Component = (*Log)(nil)
|
||||||
|
|
@ -47,8 +51,10 @@ var _ model.Component = (*Log)(nil)
|
||||||
// NewLog returns a new viewer.
|
// NewLog returns a new viewer.
|
||||||
func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log {
|
func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log {
|
||||||
l := Log{
|
l := Log{
|
||||||
Flex: tview.NewFlex(),
|
Flex: tview.NewFlex(),
|
||||||
model: model.NewLog(gvr, opts, flushTimeout),
|
logChan: make(dao.LogChan, 2),
|
||||||
|
model: model.NewLog(gvr, opts, flushTimeout),
|
||||||
|
follow: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &l
|
return &l
|
||||||
|
|
@ -76,21 +82,18 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
l.logs.SetBorderPadding(0, 0, 1, 1)
|
l.logs.SetBorderPadding(0, 0, 1, 1)
|
||||||
l.logs.SetText(logMessage)
|
l.logs.SetText("[orange::d]" + logMessage)
|
||||||
l.logs.SetWrap(l.app.Config.K9s.Logger.TextWrap)
|
l.logs.SetWrap(l.app.Config.K9s.Logger.TextWrap)
|
||||||
l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize)
|
l.logs.SetMaxLines(l.app.Config.K9s.Logger.BufferSize)
|
||||||
l.logs.cmdBuff.AddListener(l)
|
|
||||||
|
|
||||||
l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String())
|
l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String())
|
||||||
l.AddItem(l.logs, 0, 1, true)
|
l.AddItem(l.logs, 0, 1, true)
|
||||||
l.bindKeys()
|
l.bindKeys()
|
||||||
|
|
||||||
l.StylesChanged(l.app.Styles)
|
l.StylesChanged(l.app.Styles)
|
||||||
l.app.Styles.AddListener(l)
|
|
||||||
l.goFullScreen()
|
l.goFullScreen()
|
||||||
|
|
||||||
l.model.Init(l.app.factory)
|
l.model.Init(l.app.factory)
|
||||||
l.model.AddListener(l)
|
|
||||||
l.updateTitle()
|
l.updateTitle()
|
||||||
|
|
||||||
l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime)
|
l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime)
|
||||||
|
|
@ -103,6 +106,30 @@ func (l *Log) InCmdMode() bool {
|
||||||
return l.logs.cmdBuff.InCmdMode()
|
return l.logs.cmdBuff.InCmdMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogCanceled indicates no more logs are coming.
|
||||||
|
func (l *Log) LogCanceled() {
|
||||||
|
log.Debug().Msgf("LOGS_CANCELED!!!")
|
||||||
|
l.Flush([][]byte{[]byte("\n🏁 [red::b]Stream exited! No more logs...")})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStop disables log flushes.
|
||||||
|
func (l *Log) LogStop() {
|
||||||
|
log.Debug().Msgf("LOG_STOP!!!")
|
||||||
|
l.mx.Lock()
|
||||||
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
|
l.cancelUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogResume resume log flushes.
|
||||||
|
func (l *Log) LogResume() {
|
||||||
|
l.mx.Lock()
|
||||||
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
|
log.Debug().Msgf("LOG_RESUME!!!")
|
||||||
|
l.cancelUpdates = false
|
||||||
|
}
|
||||||
|
|
||||||
// LogCleared clears the logs.
|
// LogCleared clears the logs.
|
||||||
func (l *Log) LogCleared() {
|
func (l *Log) LogCleared() {
|
||||||
l.app.QueueUpdateDraw(func() {
|
l.app.QueueUpdateDraw(func() {
|
||||||
|
|
@ -126,6 +153,9 @@ func (l *Log) LogFailed(err error) {
|
||||||
// LogChanged updates the logs.
|
// LogChanged updates the logs.
|
||||||
func (l *Log) LogChanged(lines [][]byte) {
|
func (l *Log) LogChanged(lines [][]byte) {
|
||||||
l.app.QueueUpdateDraw(func() {
|
l.app.QueueUpdateDraw(func() {
|
||||||
|
if l.logs.GetText(true) == logMessage {
|
||||||
|
l.logs.Clear()
|
||||||
|
}
|
||||||
l.Flush(lines)
|
l.Flush(lines)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -166,15 +196,43 @@ func (l *Log) ExtraHints() map[string]string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Log) getContext() context.Context {
|
||||||
|
if l.cancelFn != nil {
|
||||||
|
l.cancelFn()
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, l.cancelFn = context.WithCancel(ctx)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
// Start runs the component.
|
// Start runs the component.
|
||||||
func (l *Log) Start() {
|
func (l *Log) Start() {
|
||||||
l.model.Start()
|
log.Debug().Msgf("LOG_VIEW STARTED!!")
|
||||||
|
|
||||||
|
l.model.Restart(l.getContext(), l.logChan, true)
|
||||||
|
l.model.AddListener(l)
|
||||||
|
l.app.Styles.AddListener(l)
|
||||||
|
l.logs.cmdBuff.AddListener(l)
|
||||||
|
l.logs.cmdBuff.AddListener(l.app.Prompt())
|
||||||
|
l.updateTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop terminates the component.
|
// Stop terminates the component.
|
||||||
func (l *Log) Stop() {
|
func (l *Log) Stop() {
|
||||||
l.model.Stop()
|
log.Debug().Msgf("LOG_VIEW STOPPED!")
|
||||||
l.model.RemoveListener(l)
|
l.model.RemoveListener(l)
|
||||||
|
l.model.Stop()
|
||||||
|
log.Debug().Msgf("CLOSING LOG_CHANNEL!!!")
|
||||||
|
l.mx.Lock()
|
||||||
|
{
|
||||||
|
if l.cancelFn != nil {
|
||||||
|
l.cancelFn()
|
||||||
|
l.cancelFn = nil
|
||||||
|
}
|
||||||
|
close(l.logChan)
|
||||||
|
l.logChan = nil
|
||||||
|
}
|
||||||
|
l.mx.Unlock()
|
||||||
l.app.Styles.RemoveListener(l)
|
l.app.Styles.RemoveListener(l)
|
||||||
l.logs.cmdBuff.RemoveListener(l)
|
l.logs.cmdBuff.RemoveListener(l)
|
||||||
l.logs.cmdBuff.RemoveListener(l.app.Prompt())
|
l.logs.cmdBuff.RemoveListener(l.app.Prompt())
|
||||||
|
|
@ -185,12 +243,13 @@ func (l *Log) Name() string { return logTitle }
|
||||||
|
|
||||||
func (l *Log) bindKeys() {
|
func (l *Log) bindKeys() {
|
||||||
l.logs.Actions().Set(ui.KeyActions{
|
l.logs.Actions().Set(ui.KeyActions{
|
||||||
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
|
ui.Key0: ui.NewKeyAction("tail", l.sinceCmd(-1), true),
|
||||||
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
|
ui.Key1: ui.NewKeyAction("head", l.head(), true),
|
||||||
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
|
ui.Key2: ui.NewKeyAction("1m", l.sinceCmd(60), true),
|
||||||
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
|
ui.Key3: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
|
||||||
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
|
ui.Key4: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
|
||||||
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
|
ui.Key5: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
|
||||||
|
ui.Key6: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
|
||||||
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
|
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
|
||||||
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
|
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
|
||||||
ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
||||||
|
|
@ -242,13 +301,17 @@ func (l *Log) Indicator() *LogIndicator {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) updateTitle() {
|
func (l *Log) updateTitle() {
|
||||||
sinceSeconds, since := l.model.SinceSeconds(), "all"
|
sinceSeconds, since := l.model.SinceSeconds(), "tail"
|
||||||
if sinceSeconds > 0 && sinceSeconds < 60*60 {
|
if sinceSeconds > 0 && sinceSeconds < 60*60 {
|
||||||
since = fmt.Sprintf("%dm", sinceSeconds/60)
|
since = fmt.Sprintf("%dm", sinceSeconds/60)
|
||||||
}
|
}
|
||||||
if sinceSeconds >= 60*60 {
|
if sinceSeconds >= 60*60 {
|
||||||
since = fmt.Sprintf("%dh", sinceSeconds/(60*60))
|
since = fmt.Sprintf("%dh", sinceSeconds/(60*60))
|
||||||
}
|
}
|
||||||
|
if l.model.IsHead() {
|
||||||
|
since = "head"
|
||||||
|
}
|
||||||
|
|
||||||
var title string
|
var title string
|
||||||
path, co := l.model.GetPath(), l.model.GetContainer()
|
path, co := l.model.GetPath(), l.model.GetContainer()
|
||||||
if co == "" {
|
if co == "" {
|
||||||
|
|
@ -274,25 +337,47 @@ var EOL = []byte{'\n'}
|
||||||
|
|
||||||
// Flush write logs to viewer.
|
// Flush write logs to viewer.
|
||||||
func (l *Log) Flush(lines [][]byte) {
|
func (l *Log) Flush(lines [][]byte) {
|
||||||
log.Debug().Msgf("LINES [%d]%d", runtime.NumGoroutine(), len(strings.Split(l.logs.GetText(true), "\n")))
|
defer func() {
|
||||||
if !l.indicator.AutoScroll() {
|
if l.cancelUpdates {
|
||||||
|
l.cancelUpdates = false
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(lines) == 0 || !l.indicator.AutoScroll() || l.cancelUpdates {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = l.ansiWriter.Write(EOL)
|
for i := 0; i < len(lines); i++ {
|
||||||
if _, err := l.ansiWriter.Write(bytes.Join(lines, EOL)); err != nil {
|
if l.cancelUpdates {
|
||||||
log.Error().Err(err).Msgf("write logs failed")
|
break
|
||||||
|
}
|
||||||
|
_, _ = l.ansiWriter.Write(lines[i])
|
||||||
|
}
|
||||||
|
if l.follow {
|
||||||
|
l.logs.ScrollToEnd()
|
||||||
}
|
}
|
||||||
l.logs.ScrollToEnd()
|
|
||||||
l.indicator.Refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Actions()...
|
// Actions()...
|
||||||
|
|
||||||
|
func (l *Log) head() func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
log.Debug().Msgf("!!!!HEAD!!!!")
|
||||||
|
l.cancelUpdates = true
|
||||||
|
l.logs.Clear()
|
||||||
|
l.model.Head(l.getContext(), l.logChan)
|
||||||
|
l.updateTitle()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey {
|
func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
l.model.SetSinceSeconds(int64(a))
|
l.logs.Clear()
|
||||||
|
l.model.SetSinceSeconds(l.getContext(), l.logChan, int64(a))
|
||||||
l.updateTitle()
|
l.updateTitle()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +387,7 @@ func (l *Log) toggleAllContainers(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
l.indicator.ToggleAllContainers()
|
l.indicator.ToggleAllContainers()
|
||||||
l.model.ToggleAllContainers()
|
l.model.ToggleAllContainers(l.getContext(), l.logChan)
|
||||||
l.updateTitle()
|
l.updateTitle()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -322,7 +407,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
// SaveCmd dumps the logs to file.
|
// SaveCmd dumps the logs to file.
|
||||||
func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey {
|
func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil {
|
if path, err := saveData(l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true)); err != nil {
|
||||||
l.app.Flash().Err(err)
|
l.app.Flash().Err(err)
|
||||||
} else {
|
} else {
|
||||||
l.app.Flash().Infof("Log %s saved successfully!", path)
|
l.app.Flash().Infof("Log %s saved successfully!", path)
|
||||||
|
|
@ -377,7 +462,9 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey {
|
func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
_, _, w, _ := l.GetRect()
|
_, _, w, _ := l.GetRect()
|
||||||
fmt.Fprintf(l.ansiWriter, "\n[white::b]%s[::]", strings.Repeat("─", w-4))
|
fmt.Fprintf(l.ansiWriter, "\n[white:-:b]%s[-:-:-]", strings.Repeat("─", w-4))
|
||||||
|
l.follow = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -388,6 +475,7 @@ func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
l.indicator.ToggleTimestamp()
|
l.indicator.ToggleTimestamp()
|
||||||
l.model.ToggleShowTimestamp(l.indicator.showTime)
|
l.model.ToggleShowTimestamp(l.indicator.showTime)
|
||||||
|
l.indicator.Refresh()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -399,6 +487,8 @@ func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
l.indicator.ToggleTextWrap()
|
l.indicator.ToggleTextWrap()
|
||||||
l.logs.SetWrap(l.indicator.textWrap)
|
l.logs.SetWrap(l.indicator.textWrap)
|
||||||
|
l.indicator.Refresh()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,11 +499,15 @@ func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
l.indicator.ToggleAutoScroll()
|
l.indicator.ToggleAutoScroll()
|
||||||
if l.indicator.AutoScroll() {
|
l.follow = l.indicator.AutoScroll()
|
||||||
l.model.Start()
|
// if l.indicator.AutoScroll() {
|
||||||
} else {
|
|
||||||
l.model.Stop()
|
// // l.model.Restart(l.getContext(), l.logChan, false)
|
||||||
}
|
// } else {
|
||||||
|
// // l.model.Stop()
|
||||||
|
// }
|
||||||
|
l.indicator.Refresh()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,6 +517,8 @@ func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
l.indicator.ToggleFullScreen()
|
l.indicator.ToggleFullScreen()
|
||||||
l.goFullScreen()
|
l.goFullScreen()
|
||||||
|
l.indicator.Refresh()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,7 @@ import (
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const spacer = " "
|
||||||
autoscroll = "Autoscroll"
|
|
||||||
fullscreen = "FullScreen"
|
|
||||||
timestamp = "Timestamps"
|
|
||||||
wrap = "Wrap"
|
|
||||||
allContainers = "AllContainers"
|
|
||||||
on = "[limegreen::]On"
|
|
||||||
off = "[gray::]Off"
|
|
||||||
spacer = " "
|
|
||||||
bold = "[-::b]"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LogIndicator represents a log view indicator.
|
// LogIndicator represents a log view indicator.
|
||||||
type LogIndicator struct {
|
type LogIndicator struct {
|
||||||
|
|
@ -25,6 +15,7 @@ type LogIndicator struct {
|
||||||
|
|
||||||
styles *config.Styles
|
styles *config.Styles
|
||||||
scrollStatus int32
|
scrollStatus int32
|
||||||
|
indicator []byte
|
||||||
fullScreen bool
|
fullScreen bool
|
||||||
textWrap bool
|
textWrap bool
|
||||||
showTime bool
|
showTime bool
|
||||||
|
|
@ -33,15 +24,16 @@ type LogIndicator struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogIndicator returns a new indicator.
|
// NewLogIndicator returns a new indicator.
|
||||||
func NewLogIndicator(cfg *config.Config, styles *config.Styles, isContainerLogView bool) *LogIndicator {
|
func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bool) *LogIndicator {
|
||||||
l := LogIndicator{
|
l := LogIndicator{
|
||||||
styles: styles,
|
styles: styles,
|
||||||
TextView: tview.NewTextView(),
|
TextView: tview.NewTextView(),
|
||||||
|
indicator: make([]byte, 0, 100),
|
||||||
scrollStatus: 1,
|
scrollStatus: 1,
|
||||||
fullScreen: cfg.K9s.Logger.FullScreenLogs,
|
fullScreen: cfg.K9s.Logger.FullScreenLogs,
|
||||||
textWrap: cfg.K9s.Logger.TextWrap,
|
textWrap: cfg.K9s.Logger.TextWrap,
|
||||||
showTime: cfg.K9s.Logger.ShowTime,
|
showTime: cfg.K9s.Logger.ShowTime,
|
||||||
shouldDisplayAllContainers: isContainerLogView,
|
shouldDisplayAllContainers: allContainers,
|
||||||
}
|
}
|
||||||
l.StylesChanged(styles)
|
l.StylesChanged(styles)
|
||||||
styles.AddListener(&l)
|
styles.AddListener(&l)
|
||||||
|
|
@ -110,24 +102,46 @@ func (l *LogIndicator) ToggleAllContainers() {
|
||||||
l.Refresh()
|
l.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh updates the view.
|
func (l *LogIndicator) reset() {
|
||||||
func (l *LogIndicator) Refresh() {
|
|
||||||
l.Clear()
|
l.Clear()
|
||||||
if l.shouldDisplayAllContainers {
|
l.indicator = l.indicator[:0]
|
||||||
l.update(allContainers, l.allContainers, spacer)
|
|
||||||
}
|
|
||||||
l.update(autoscroll, l.AutoScroll(), spacer)
|
|
||||||
l.update(fullscreen, l.fullScreen, spacer)
|
|
||||||
l.update(timestamp, l.showTime, spacer)
|
|
||||||
l.update(wrap, l.textWrap, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LogIndicator) update(title string, state bool, padding string) {
|
// Refresh updates the view.
|
||||||
bb := []byte(bold + title + ":")
|
func (l *LogIndicator) Refresh() {
|
||||||
if state {
|
l.reset()
|
||||||
bb = append(bb, []byte(on)...)
|
|
||||||
} else {
|
if l.shouldDisplayAllContainers {
|
||||||
bb = append(bb, []byte(off)...)
|
if l.allContainers {
|
||||||
|
l.indicator = append(l.indicator, "[::b]AllContainers:[limegreen::b]On[-::]"+spacer...)
|
||||||
|
} else {
|
||||||
|
l.indicator = append(l.indicator, "[::b]AllContainers:[gray::d]Off[-::]"+spacer...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, _ = l.Write(append(bb, []byte(padding)...))
|
|
||||||
|
if l.AutoScroll() {
|
||||||
|
l.indicator = append(l.indicator, "[::b]Autoscroll:[limegreen::b]On[-::]"+spacer...)
|
||||||
|
} else {
|
||||||
|
l.indicator = append(l.indicator, "[::b]Autoscroll:[gray::d]Off[-::]"+spacer...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.FullScreen() {
|
||||||
|
l.indicator = append(l.indicator, "[::b]FullScreen:[limegreen::b]On[-::]"+spacer...)
|
||||||
|
} else {
|
||||||
|
l.indicator = append(l.indicator, "[::b]FullScreen:[gray::d]Off[-::]"+spacer...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.Timestamp() {
|
||||||
|
l.indicator = append(l.indicator, "[::b]Timestamps:[limegreen::b]On[-::]"+spacer...)
|
||||||
|
} else {
|
||||||
|
l.indicator = append(l.indicator, "[::b]Timestamps:[gray::d]Off[-::]"+spacer...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.TextWrap() {
|
||||||
|
l.indicator = append(l.indicator, "[::b]Wrap:[limegreen::b]On[-::]"...)
|
||||||
|
} else {
|
||||||
|
l.indicator = append(l.indicator, "[::b]Wrap:[gray::d]Off[-::]"...)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = l.Write(l.indicator)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@ func TestLogIndicatorRefresh(t *testing.T) {
|
||||||
li *view.LogIndicator
|
li *view.LogIndicator
|
||||||
e string
|
e string
|
||||||
}{
|
}{
|
||||||
"all containers": {
|
"all-containers": {
|
||||||
view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[-::b]AllContainers:[gray::]Off [-::b]Autoscroll:[limegreen::]On [-::b]FullScreen:[gray::]Off [-::b]Timestamps:[gray::]Off [-::b]Wrap:[gray::]Off\n",
|
view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
|
||||||
},
|
},
|
||||||
"no all containers": {
|
"plain": {
|
||||||
view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[-::b]Autoscroll:[limegreen::]On [-::b]FullScreen:[gray::]Off [-::b]Timestamps:[gray::]Off [-::b]Wrap:[gray::]Off\n",
|
view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,7 +26,18 @@ func TestLogIndicatorRefresh(t *testing.T) {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
u.li.Refresh()
|
u.li.Refresh()
|
||||||
assert.Equal(t, u.li.GetText(false), u.e)
|
assert.Equal(t, u.e, u.li.GetText(false))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkLogIndicatorRefresh(b *testing.B) {
|
||||||
|
defaults := config.NewStyles()
|
||||||
|
v := view.NewLogIndicator(config.NewConfig(nil), defaults, true)
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
v.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func TestLogAutoScroll(t *testing.T) {
|
||||||
v.GetModel().Set(ii)
|
v.GetModel().Set(ii)
|
||||||
v.GetModel().Notify()
|
v.GetModel().Notify()
|
||||||
|
|
||||||
assert.Equal(t, 15, len(v.Hints()))
|
assert.Equal(t, 16, len(v.Hints()))
|
||||||
|
|
||||||
v.toggleAutoScrollCmd(nil)
|
v.toggleAutoScrollCmd(nil)
|
||||||
assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true))
|
assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true))
|
||||||
|
|
@ -75,8 +75,7 @@ func TestLogTimestamp(t *testing.T) {
|
||||||
&dao.LogItem{
|
&dao.LogItem{
|
||||||
Pod: "fred/blee",
|
Pod: "fred/blee",
|
||||||
Container: "c1",
|
Container: "c1",
|
||||||
Timestamp: "ttt",
|
Bytes: []byte("ttt Testing 1, 2, 3\n"),
|
||||||
Bytes: []byte("Testing 1, 2, 3"),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
var list logList
|
var list logList
|
||||||
|
|
@ -84,9 +83,11 @@ func TestLogTimestamp(t *testing.T) {
|
||||||
l.GetModel().Set(ii)
|
l.GetModel().Set(ii)
|
||||||
l.SendKeys(ui.KeyT)
|
l.SendKeys(ui.KeyT)
|
||||||
l.Logs().Clear()
|
l.Logs().Clear()
|
||||||
l.Flush(ii.Lines(true))
|
ll := make([][]byte, ii.Len())
|
||||||
|
ii.Lines(0, true, ll)
|
||||||
|
l.Flush(ll)
|
||||||
|
|
||||||
assert.Equal(t, fmt.Sprintf("\n%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3"), l.Logs().GetText(true))
|
assert.Equal(t, fmt.Sprintf("%-30s %s", "ttt", "fred/blee c1 Testing 1, 2, 3\n"), l.Logs().GetText(true))
|
||||||
assert.Equal(t, 2, list.change)
|
assert.Equal(t, 2, list.change)
|
||||||
assert.Equal(t, 2, list.clear)
|
assert.Equal(t, 2, list.clear)
|
||||||
assert.Equal(t, 0, list.fail)
|
assert.Equal(t, 0, list.fail)
|
||||||
|
|
@ -131,5 +132,8 @@ func (l *logList) LogChanged(ll [][]byte) {
|
||||||
l.lines += string(line)
|
l.lines += string(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func (l *logList) LogCanceled() {}
|
||||||
|
func (l *logList) LogStop() {}
|
||||||
|
func (l *logList) LogResume() {}
|
||||||
func (l *logList) LogCleared() { l.clear++ }
|
func (l *logList) LogCleared() { l.clear++ }
|
||||||
func (l *logList) LogFailed(error) { l.fail++ }
|
func (l *logList) LogFailed(error) { l.fail++ }
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package view_test
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -25,10 +25,32 @@ func TestLog(t *testing.T) {
|
||||||
v.Init(makeContext())
|
v.Init(makeContext())
|
||||||
|
|
||||||
ii := dao.NewLogItems()
|
ii := dao.NewLogItems()
|
||||||
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo"))
|
ii.Add(dao.NewLogItemFromString("blee\n"), dao.NewLogItemFromString("bozo\n"))
|
||||||
v.Flush(ii.Lines(false))
|
ll := make([][]byte, ii.Len())
|
||||||
|
ii.Lines(0, false, ll)
|
||||||
|
v.Flush(ll)
|
||||||
|
|
||||||
assert.Equal(t, 29, len(v.Logs().GetText(true)))
|
assert.Equal(t, "Waiting for logs...\nblee\nbozo\n", v.Logs().GetText(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogFlush(t *testing.T) {
|
||||||
|
opts := dao.LogOptions{
|
||||||
|
Path: "fred/p1",
|
||||||
|
Container: "blee",
|
||||||
|
}
|
||||||
|
v := view.NewLog(client.NewGVR("v1/pods"), &opts)
|
||||||
|
v.Init(makeContext())
|
||||||
|
|
||||||
|
items := dao.NewLogItems()
|
||||||
|
items.Add(
|
||||||
|
dao.NewLogItemFromString("\033[0;30mblee\n"),
|
||||||
|
dao.NewLogItemFromString("\033[0;32mBozo\n"),
|
||||||
|
)
|
||||||
|
ll := make([][]byte, items.Len())
|
||||||
|
items.Lines(0, false, ll)
|
||||||
|
v.Flush(ll)
|
||||||
|
|
||||||
|
assert.Equal(t, "[orange::d]Waiting for logs...\n[black:]blee\n[green:]Bozo\n\n", v.Logs().GetText(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkLogFlush(b *testing.B) {
|
func BenchmarkLogFlush(b *testing.B) {
|
||||||
|
|
@ -41,13 +63,17 @@ func BenchmarkLogFlush(b *testing.B) {
|
||||||
|
|
||||||
items := dao.NewLogItems()
|
items := dao.NewLogItems()
|
||||||
items.Add(
|
items.Add(
|
||||||
dao.NewLogItemFromString("blee"),
|
dao.NewLogItemFromString("\033[0;30mblee\n"),
|
||||||
dao.NewLogItemFromString("bozo"),
|
dao.NewLogItemFromString("\033[0;101mBozo\n"),
|
||||||
|
dao.NewLogItemFromString("\033[0;101mBozo\n"),
|
||||||
)
|
)
|
||||||
|
ll := make([][]byte, items.Len())
|
||||||
|
items.Lines(0, false, ll)
|
||||||
|
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
v.Flush(items.Lines(false))
|
v.Flush(ll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,12 +102,15 @@ func TestLogViewSave(t *testing.T) {
|
||||||
app := makeApp()
|
app := makeApp()
|
||||||
ii := dao.NewLogItems()
|
ii := dao.NewLogItems()
|
||||||
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo"))
|
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo"))
|
||||||
v.Flush(ii.Lines(false))
|
ll := make([][]byte, ii.Len())
|
||||||
|
ii.Lines(0, false, ll)
|
||||||
|
v.Flush(ll)
|
||||||
|
|
||||||
config.K9sDumpDir = "/tmp"
|
config.K9sDumpDir = "/tmp"
|
||||||
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
|
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
|
||||||
c1, _ := ioutil.ReadDir(dir)
|
c1, _ := os.ReadDir(dir)
|
||||||
v.SaveCmd(nil)
|
v.SaveCmd(nil)
|
||||||
c2, _ := ioutil.ReadDir(dir)
|
c2, _ := os.ReadDir(dir)
|
||||||
assert.Equal(t, len(c2), len(c1)+1)
|
assert.Equal(t, len(c2), len(c1)+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,7 @@ func (l *Logger) Init(_ context.Context) error {
|
||||||
func (l *Logger) BufferChanged(s string) {}
|
func (l *Logger) BufferChanged(s string) {}
|
||||||
|
|
||||||
// BufferCompleted indicates input was accepted.
|
// BufferCompleted indicates input was accepted.
|
||||||
func (l *Logger) BufferCompleted(s string) {
|
func (l *Logger) BufferCompleted(s string) {}
|
||||||
}
|
|
||||||
|
|
||||||
// BufferActive indicates the buff activity changed.
|
// BufferActive indicates the buff activity changed.
|
||||||
func (l *Logger) BufferActive(state bool, k model.BufferKind) {
|
func (l *Logger) BufferActive(state bool, k model.BufferKind) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package view
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
|
|
@ -138,19 +139,35 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
path := p.GetTable().GetSelectedItem()
|
selections := p.GetTable().GetSelectedItems()
|
||||||
if path == "" {
|
if len(selections) == 0 {
|
||||||
return nil
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", path), func() {
|
p.Stop()
|
||||||
var pf dao.PortForward
|
defer p.Start()
|
||||||
pf.Init(p.App().factory, client.NewGVR("portforwards"))
|
var msg string
|
||||||
if err := pf.Delete(path, true, true); err != nil {
|
if len(selections) > 1 {
|
||||||
|
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), p.GVR())
|
||||||
|
} else {
|
||||||
|
h, err := pfToHuman(selections[0])
|
||||||
|
if err == nil {
|
||||||
|
msg = fmt.Sprintf("Delete %s %s?", p.GVR().R(), h)
|
||||||
|
} else {
|
||||||
p.App().Flash().Err(err)
|
p.App().Flash().Err(err)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
p.App().Flash().Infof("PortForward %s deleted!", path)
|
}
|
||||||
|
showModal(p.App().Content.Pages, msg, func() {
|
||||||
|
for _, s := range selections {
|
||||||
|
var pf dao.PortForward
|
||||||
|
pf.Init(p.App().factory, client.NewGVR("portforwards"))
|
||||||
|
if err := pf.Delete(s, true, true); err != nil {
|
||||||
|
p.App().Flash().Err(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.App().Flash().Infof("Successfully deleted %d PortForward!", len(selections))
|
||||||
p.GetTable().Refresh()
|
p.GetTable().Refresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -160,6 +177,16 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
var selRx = regexp.MustCompile(`\A([\w-]+)/([\w-]+)\|([\w-]+)\|(\d+):(\d+)`)
|
||||||
|
|
||||||
|
func pfToHuman(s string) (string, error) {
|
||||||
|
mm := selRx.FindStringSubmatch(s)
|
||||||
|
if len(mm) < 6 {
|
||||||
|
return "", fmt.Errorf("Unable to parse selection %s", s)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s::%s %s->%s", mm[2], mm[3], mm[4], mm[5]), nil
|
||||||
|
}
|
||||||
|
|
||||||
func showModal(p *ui.Pages, msg string, ok func()) {
|
func showModal(p *ui.Pages, msg string, ok func()) {
|
||||||
m := tview.NewModal().
|
m := tview.NewModal().
|
||||||
AddButtons([]string{"Cancel", "OK"}).
|
AddButtons([]string{"Cancel", "OK"}).
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,20 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/port"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const portForwardKey = "portforward"
|
const portForwardKey = "portforward"
|
||||||
|
|
||||||
// PortForwardCB represents a port-forward callback function.
|
// PortForwardCB represents a port-forward callback function.
|
||||||
type PortForwardCB func(v ResourceViewer, path, co string, mapper []client.PortTunnel)
|
type PortForwardCB func(ResourceViewer, string, port.PortTunnels) error
|
||||||
|
|
||||||
// ShowPortForwards pops a port forwarding configuration dialog.
|
// ShowPortForwards pops a port forwarding configuration dialog.
|
||||||
func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string, okFn PortForwardCB) {
|
func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpecs, aa port.Annotations, okFn PortForwardCB) {
|
||||||
styles := v.App().Styles.Dialog()
|
styles := v.App().Styles.Dialog()
|
||||||
|
|
||||||
f := tview.NewForm()
|
f := tview.NewForm()
|
||||||
|
|
@ -32,37 +34,28 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string,
|
||||||
|
|
||||||
address := v.App().Config.CurrentCluster().PortForwardAddress
|
address := v.App().Config.CurrentCluster().PortForwardAddress
|
||||||
|
|
||||||
var p1, p2 string
|
pf, err := aa.PreferredPorts(ports)
|
||||||
if len(ports) > 0 {
|
if err != nil {
|
||||||
p1, p2 = ports[0], extractPort(ports[0])
|
log.Warn().Err(err).Msgf("unable to resolve ports")
|
||||||
if len(ann) != 0 {
|
|
||||||
container, port, ok := parsePFAnn(ann)
|
|
||||||
if ok {
|
|
||||||
for _, p := range ports {
|
|
||||||
co, po, portNum := parsePort(p)
|
|
||||||
if co == container && port == po || port == portNum {
|
|
||||||
p1, p2 = p, extractPort(p)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p1, p2 := pf.ToPortSpec(ports)
|
||||||
fieldLen := int(math.Max(30, float64(len(p1))))
|
fieldLen := int(math.Max(30, float64(len(p1))))
|
||||||
f.AddInputField("Container Port:", p1, fieldLen, nil, func(p string) {
|
f.AddInputField("Container Port:", p1, fieldLen, nil, nil)
|
||||||
p1 = p
|
coField := f.GetFormItemByLabel("Container Port:").(*tview.InputField)
|
||||||
})
|
if coField.GetText() == "" {
|
||||||
field := f.GetFormItemByLabel("Container Port:").(*tview.InputField)
|
coField.SetPlaceholder("Enter a container name/port")
|
||||||
if field.GetText() == "" {
|
|
||||||
field.SetPlaceholder("Enter a container name/port")
|
|
||||||
}
|
}
|
||||||
f.AddInputField("Local Port:", p2, fieldLen, nil, func(p string) {
|
f.AddInputField("Local Port:", p2, fieldLen, nil, nil)
|
||||||
p2 = p
|
poField := f.GetFormItemByLabel("Local Port:").(*tview.InputField)
|
||||||
})
|
if poField.GetText() == "" {
|
||||||
field = f.GetFormItemByLabel("Local Port:").(*tview.InputField)
|
poField.SetPlaceholder("Enter a local port")
|
||||||
if field.GetText() == "" {
|
|
||||||
field.SetPlaceholder("Enter a local port")
|
|
||||||
}
|
}
|
||||||
|
coField.SetChangedFunc(func(s string) {
|
||||||
|
port := extractPort(s)
|
||||||
|
poField.SetText(port)
|
||||||
|
p2 = port
|
||||||
|
})
|
||||||
f.AddInputField("Address:", address, fieldLen, nil, func(h string) {
|
f.AddInputField("Address:", address, fieldLen, nil, func(h string) {
|
||||||
address = h
|
address = h
|
||||||
})
|
})
|
||||||
|
|
@ -76,21 +69,18 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string,
|
||||||
}
|
}
|
||||||
|
|
||||||
f.AddButton("OK", func() {
|
f.AddButton("OK", func() {
|
||||||
pp1 := strings.Split(p1, ",")
|
if coField.GetText() == "" || poField.GetText() == "" {
|
||||||
pp2 := strings.Split(p2, ",")
|
|
||||||
if len(pp1) == 0 || len(pp1) != len(pp2) {
|
|
||||||
v.App().Flash().Err(fmt.Errorf("container to local port mismatch"))
|
v.App().Flash().Err(fmt.Errorf("container to local port mismatch"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var tt []client.PortTunnel
|
tt, err := port.ToTunnels(address, coField.GetText(), poField.GetText())
|
||||||
for i := range pp1 {
|
if err != nil {
|
||||||
tt = append(tt, client.PortTunnel{
|
v.App().Flash().Err(err)
|
||||||
Address: address,
|
return
|
||||||
LocalPort: pp2[i],
|
}
|
||||||
ContainerPort: extractPort(pp1[i]),
|
if err := okFn(v, path, tt); err != nil {
|
||||||
})
|
v.App().Flash().Err(err)
|
||||||
}
|
}
|
||||||
okFn(v, path, extractContainer(pp1[0]), tt)
|
|
||||||
})
|
})
|
||||||
pages := v.App().Content.Pages
|
pages := v.App().Content.Pages
|
||||||
f.AddButton("Cancel", func() {
|
f.AddButton("Cancel", func() {
|
||||||
|
|
@ -108,7 +98,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string,
|
||||||
modal := tview.NewModalForm("<PortForward>", f)
|
modal := tview.NewModalForm("<PortForward>", f)
|
||||||
msg := path
|
msg := path
|
||||||
if len(ports) > 1 {
|
if len(ports) > 1 {
|
||||||
msg += "\n\nExposed Ports:\n" + strings.Join(ports, "\n")
|
msg += "\n\nExposed Ports:\n" + ports.Dump()
|
||||||
}
|
}
|
||||||
modal.SetText(msg)
|
modal.SetText(msg)
|
||||||
modal.SetTextColor(styles.FgColor.Color())
|
modal.SetTextColor(styles.FgColor.Color())
|
||||||
|
|
@ -131,16 +121,6 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func parsePort(p string) (string, string, string) {
|
|
||||||
rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(╱UDP)?\z`)
|
|
||||||
mm := rx.FindStringSubmatch(p)
|
|
||||||
if len(mm) != 5 {
|
|
||||||
return "", "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return mm[1], mm[2], mm[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractPort(p string) string {
|
func extractPort(p string) string {
|
||||||
rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(╱UDP)?\z`)
|
rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(╱UDP)?\z`)
|
||||||
mm := rx.FindStringSubmatch(p)
|
mm := rx.FindStringSubmatch(p)
|
||||||
|
|
|
||||||
|
|
@ -6,44 +6,6 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExtractPort(t *testing.T) {
|
|
||||||
uu := map[string]struct {
|
|
||||||
port, e string
|
|
||||||
}{
|
|
||||||
"empty": {
|
|
||||||
"", "",
|
|
||||||
},
|
|
||||||
"full": {
|
|
||||||
"co/fred:8000", "8000",
|
|
||||||
},
|
|
||||||
"named": {
|
|
||||||
"fred:8000", "8000",
|
|
||||||
},
|
|
||||||
"port": {
|
|
||||||
"8000", "8000",
|
|
||||||
},
|
|
||||||
"protocol": {
|
|
||||||
"dns:53╱UDP", "53",
|
|
||||||
},
|
|
||||||
"unamed": {
|
|
||||||
"dns/53", "53",
|
|
||||||
},
|
|
||||||
"pod-dashed": {
|
|
||||||
"blee-fred/:5000", "5000",
|
|
||||||
},
|
|
||||||
"co-dashed": {
|
|
||||||
"blee/fred-doh:5000", "5000",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k := range uu {
|
|
||||||
u := uu[k]
|
|
||||||
t.Run(k, func(t *testing.T) {
|
|
||||||
assert.Equal(t, u.e, extractPort(u.port))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractContainer(t *testing.T) {
|
func TestExtractContainer(t *testing.T) {
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
port, e string
|
port, e string
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
|
"github.com/derailed/k9s/internal/port"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
"github.com/derailed/k9s/internal/watch"
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
|
@ -19,8 +16,6 @@ import (
|
||||||
"k8s.io/client-go/tools/portforward"
|
"k8s.io/client-go/tools/portforward"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AnnDefaultPF = "k9s.imhotep.io/default-portforward-container"
|
|
||||||
|
|
||||||
// PortForwardExtender adds port-forward extensions.
|
// PortForwardExtender adds port-forward extensions.
|
||||||
type PortForwardExtender struct {
|
type PortForwardExtender struct {
|
||||||
ResourceViewer
|
ResourceViewer
|
||||||
|
|
@ -78,19 +73,11 @@ func (p *PortForwardExtender) fetchPodName(path string) (string, error) {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func tryListenPort(address, port string) error {
|
|
||||||
server, err := net.Listen("tcp", fmt.Sprintf("%s:%s", address, port))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return server.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForwarder) {
|
func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForwarder) {
|
||||||
v.App().factory.AddForwarder(pf)
|
v.App().factory.AddForwarder(pf)
|
||||||
|
|
||||||
v.App().QueueUpdateDraw(func() {
|
v.App().QueueUpdateDraw(func() {
|
||||||
v.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
|
v.App().Flash().Infof("PortForward activated %s", pf.ID())
|
||||||
DismissPortForwards(v, v.App().Content.Pages)
|
DismissPortForwards(v, v.App().Content.Pages)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -106,67 +93,77 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func startFwdCB(v ResourceViewer, path, co string, tt []client.PortTunnel) {
|
func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error {
|
||||||
for _, t := range tt {
|
if err := pts.CheckAvailable(); err != nil {
|
||||||
err := tryListenPort(t.Address, t.LocalPort)
|
return err
|
||||||
if err != nil {
|
}
|
||||||
v.App().Flash().Err(err)
|
|
||||||
return
|
for _, pt := range pts {
|
||||||
|
if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, pt.Container, pt.PortMap())); ok {
|
||||||
|
return fmt.Errorf("A port-forward is already active on pod %s", path)
|
||||||
}
|
}
|
||||||
|
pf := dao.NewPortForwarder(v.App().factory)
|
||||||
|
fwd, err := pf.Start(path, pt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debug().Msgf(">>> Starting port forward %q -- %#v", pf.ID(), pt)
|
||||||
|
go runForward(v, pf, fwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, co)); ok {
|
return nil
|
||||||
v.App().Flash().Err(errors.New("A port-forward is already active on this pod"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pf := dao.NewPortForwarder(v.App().factory)
|
|
||||||
fwd, err := pf.Start(path, co, tt)
|
|
||||||
if err != nil {
|
|
||||||
v.App().Flash().Err(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Msgf(">>> Starting port forward %q %#v", path, tt)
|
|
||||||
go runForward(v, pf, fwd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
|
func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
|
||||||
mm, coPort, err := fetchPodPorts(v.App().factory, path)
|
mm, anns, err := fetchPodPorts(v.App().factory, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ports := make([]string, 0, len(mm))
|
ports := make(port.ContainerPortSpecs, 0, len(mm))
|
||||||
for co, pp := range mm {
|
for co, pp := range mm {
|
||||||
for _, p := range pp {
|
for _, p := range pp {
|
||||||
if p.Protocol != v1.ProtocolTCP {
|
if p.Protocol != v1.ProtocolTCP {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ports = append(ports, client.FQN(co, p.Name)+":"+strconv.Itoa(int(p.ContainerPort)))
|
ports = append(ports, port.NewPortSpec(co, p.Name, p.ContainerPort))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ShowPortForwards(v, path, ports, coPort, cb)
|
if spec, ok := anns[port.K9sAutoPortForwardsKey]; ok {
|
||||||
|
pfs, err := port.ParsePFs(spec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pts, err := pfs.ToTunnels(v.App().Config.CurrentCluster().PortForwardAddress, ports, port.IsPortFree)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return startFwdCB(v, path, pts)
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowPortForwards(v, path, ports, anns, cb)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, string, error) {
|
func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, map[string]string, error) {
|
||||||
log.Debug().Msgf("Fetching ports on pod %q", path)
|
log.Debug().Msgf("Fetching ports on pod %q", path)
|
||||||
o, err := f.Get("v1/pods", path, true, labels.Everything())
|
o, err := f.Get("v1/pods", path, true, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pod v1.Pod
|
var pod v1.Pod
|
||||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pp := make(map[string][]v1.ContainerPort)
|
pp := make(map[string][]v1.ContainerPort, len(pod.Spec.Containers))
|
||||||
for _, co := range pod.Spec.Containers {
|
for _, co := range pod.Spec.Containers {
|
||||||
pp[co.Name] = co.Ports
|
pp[co.Name] = co.Ports
|
||||||
}
|
}
|
||||||
|
|
||||||
return pp, pod.Annotations[AnnDefaultPF], nil
|
return pp, pod.Annotations, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,6 @@ func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
p.App().Flash().Infof("Delete resource %s %s", p.GVR(), selections[0])
|
p.App().Flash().Infof("Delete resource %s %s", p.GVR(), selections[0])
|
||||||
}
|
}
|
||||||
p.GetTable().ShowDeleted()
|
p.GetTable().ShowDeleted()
|
||||||
log.Debug().Msgf("SELS %v", selections)
|
|
||||||
for _, path := range selections {
|
for _, path := range selections {
|
||||||
if err := nuker.Delete(path, true, true); err != nil {
|
if err := nuker.Delete(path, true, true); err != nil {
|
||||||
p.App().Flash().Errf("Delete failed with %s", err)
|
p.App().Flash().Errf("Delete failed with %s", err)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ func (r *RestartExtender) bindKeys(aa ui.KeyActions) {
|
||||||
|
|
||||||
func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
paths := r.GetTable().GetSelectedItems()
|
paths := r.GetTable().GetSelectedItems()
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 || paths[0] == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,26 +36,30 @@ func (s *ScaleExtender) bindKeys(aa ui.KeyActions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
path := s.GetTable().GetSelectedItem()
|
paths := s.GetTable().GetSelectedItems()
|
||||||
if path == "" {
|
if len(paths) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Stop()
|
s.Stop()
|
||||||
defer s.Start()
|
defer s.Start()
|
||||||
s.showScaleDialog(path)
|
s.showScaleDialog(paths)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScaleExtender) showScaleDialog(path string) {
|
func (s *ScaleExtender) showScaleDialog(paths []string) {
|
||||||
form, err := s.makeScaleForm(path)
|
form, err := s.makeScaleForm(paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.App().Flash().Err(err)
|
s.App().Flash().Err(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
confirm := tview.NewModalForm("<Scale>", form)
|
confirm := tview.NewModalForm("<Scale>", form)
|
||||||
confirm.SetText(fmt.Sprintf("Scale %s %s", s.GVR(), path))
|
msg := fmt.Sprintf("Scale %s %s?", s.GVR().R(), paths[0])
|
||||||
|
if len(paths) > 1 {
|
||||||
|
msg = fmt.Sprintf("Scale [%d] %s?", len(paths), s.GVR().R())
|
||||||
|
}
|
||||||
|
confirm.SetText(msg)
|
||||||
confirm.SetDoneFunc(func(int, string) {
|
confirm.SetDoneFunc(func(int, string) {
|
||||||
s.dismissDialog()
|
s.dismissDialog()
|
||||||
})
|
})
|
||||||
|
|
@ -71,40 +75,49 @@ func (s *ScaleExtender) valueOf(col string) (string, error) {
|
||||||
return s.GetTable().GetSelectedCell(colIdx), nil
|
return s.GetTable().GetSelectedCell(colIdx), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScaleExtender) makeScaleForm(sel string) (*tview.Form, error) {
|
func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) {
|
||||||
f := s.makeStyledForm()
|
f := s.makeStyledForm()
|
||||||
|
|
||||||
replicas, err := s.valueOf("READY")
|
factor := "0"
|
||||||
if err != nil {
|
if len(sels) == 1 {
|
||||||
return nil, err
|
replicas, err := s.valueOf("READY")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokens := strings.Split(replicas, "/")
|
||||||
|
if len(tokens) < 2 {
|
||||||
|
return nil, fmt.Errorf("unable to locate replicas from %s", replicas)
|
||||||
|
}
|
||||||
|
factor = strings.TrimRight(tokens[1], ui.DeltaSign)
|
||||||
}
|
}
|
||||||
tokens := strings.Split(replicas, "/")
|
f.AddInputField("Replicas:", factor, 4, func(textToCheck string, lastChar rune) bool {
|
||||||
if len(tokens) < 2 {
|
|
||||||
return nil, fmt.Errorf("unable to locate replicas from %s", replicas)
|
|
||||||
}
|
|
||||||
replicas = strings.TrimRight(tokens[1], ui.DeltaSign)
|
|
||||||
f.AddInputField("Replicas:", replicas, 4, func(textToCheck string, lastChar rune) bool {
|
|
||||||
_, err := strconv.Atoi(textToCheck)
|
_, err := strconv.Atoi(textToCheck)
|
||||||
return err == nil
|
return err == nil
|
||||||
}, func(changed string) {
|
}, func(changed string) {
|
||||||
replicas = changed
|
factor = changed
|
||||||
})
|
})
|
||||||
|
|
||||||
f.AddButton("OK", func() {
|
f.AddButton("OK", func() {
|
||||||
defer s.dismissDialog()
|
defer s.dismissDialog()
|
||||||
count, err := strconv.Atoi(replicas)
|
count, err := strconv.Atoi(factor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.App().Flash().Err(err)
|
s.App().Flash().Err(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
|
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := s.scale(ctx, sel, count); err != nil {
|
for _, sel := range sels {
|
||||||
log.Error().Err(err).Msgf("DP %s scaling failed", sel)
|
if err := s.scale(ctx, sel, count); err != nil {
|
||||||
s.App().Flash().Err(err)
|
log.Error().Err(err).Msgf("DP %s scaling failed", sel)
|
||||||
return
|
s.App().Flash().Err(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(sels) == 1 {
|
||||||
|
s.App().Flash().Infof("[%d] %s scaled successfully", len(sels), s.GVR().R())
|
||||||
|
} else {
|
||||||
|
s.App().Flash().Infof("%s %s scaled successfully", s.GVR().R(), sels[0])
|
||||||
}
|
}
|
||||||
s.App().Flash().Infof("Resource %s:%s scaled successfully", s.GVR(), sel)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
f.AddButton("Cancel", func() {
|
f.AddButton("Cancel", func() {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/atotto/clipboard"
|
"github.com/atotto/clipboard"
|
||||||
|
|
@ -53,9 +54,17 @@ func (t *Table) Init(ctx context.Context) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeaderIndex returns index of a given column or false if not found.
|
// HeaderIndex returns index of a given column or false if not found.
|
||||||
func (t *Table) HeaderIndex(header string) (int, bool) {
|
func (t *Table) HeaderIndex(colName string) (int, bool) {
|
||||||
for i := 0; i < t.GetColumnCount(); i++ {
|
for i := 0; i < t.GetColumnCount(); i++ {
|
||||||
if h := t.GetCell(0, i); h != nil && h.Text == header {
|
h := t.GetCell(0, i)
|
||||||
|
if h == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := h.Text
|
||||||
|
if idx := strings.Index(s, "["); idx > 0 {
|
||||||
|
s = s[:idx]
|
||||||
|
}
|
||||||
|
if s == colName {
|
||||||
return i, true
|
return i, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -25,10 +25,10 @@ func TestTableSave(t *testing.T) {
|
||||||
v.SetTitle("k9s-test")
|
v.SetTitle("k9s-test")
|
||||||
|
|
||||||
dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster)
|
dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster)
|
||||||
c1, _ := ioutil.ReadDir(dir)
|
c1, _ := os.ReadDir(dir)
|
||||||
v.saveCmd(nil)
|
v.saveCmd(nil)
|
||||||
|
|
||||||
c2, _ := ioutil.ReadDir(dir)
|
c2, _ := os.ReadDir(dir)
|
||||||
assert.Equal(t, len(c2), len(c1)+1)
|
assert.Equal(t, len(c2), len(c1)+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -244,11 +244,8 @@ func (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, err
|
||||||
func (f *Factory) AddForwarder(pf Forwarder) {
|
func (f *Factory) AddForwarder(pf Forwarder) {
|
||||||
f.mx.Lock()
|
f.mx.Lock()
|
||||||
defer f.mx.Unlock()
|
defer f.mx.Unlock()
|
||||||
f.forwarders[pf.Path()] = pf
|
|
||||||
|
|
||||||
for k, v := range f.forwarders {
|
f.forwarders[pf.Path()] = pf
|
||||||
log.Debug().Msgf("%q -- %#v", k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteForwarder deletes portforward for a given container.
|
// DeleteForwarder deletes portforward for a given container.
|
||||||
|
|
@ -277,11 +274,20 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) {
|
||||||
return fwd, ok
|
return fwd, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BOZO!! Review!!!
|
||||||
// ValidatePortForwards check if pods are still around for portforwards.
|
// ValidatePortForwards check if pods are still around for portforwards.
|
||||||
func (f *Factory) ValidatePortForwards() {
|
func (f *Factory) ValidatePortForwards() {
|
||||||
for k, fwd := range f.forwarders {
|
for k, fwd := range f.forwarders {
|
||||||
tokens := strings.Split(k, ":")
|
tokens := strings.Split(k, ":")
|
||||||
_, err := f.Get("v1/pods", tokens[0], false, labels.Everything())
|
if len(tokens) != 2 {
|
||||||
|
log.Error().Msgf("Invalid fwd keys %q", k)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
paths := strings.Split(tokens[0], "|")
|
||||||
|
if len(paths) < 1 {
|
||||||
|
log.Error().Msgf("Invalid path %q", tokens[0])
|
||||||
|
}
|
||||||
|
_, err := f.Get("v1/pods", paths[0], false, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fwd.Stop()
|
fwd.Stop()
|
||||||
delete(f.forwarders, k)
|
delete(f.forwarders, k)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package watch
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/port"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/client-go/tools/portforward"
|
"k8s.io/client-go/tools/portforward"
|
||||||
)
|
)
|
||||||
|
|
@ -11,19 +11,22 @@ import (
|
||||||
// Forwarder represents a port forwarder.
|
// Forwarder represents a port forwarder.
|
||||||
type Forwarder interface {
|
type Forwarder interface {
|
||||||
// Start starts a port-forward.
|
// Start starts a port-forward.
|
||||||
Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error)
|
Start(path string, tunnel port.PortTunnel) (*portforward.PortForwarder, error)
|
||||||
|
|
||||||
// Stop terminates a port forward.
|
// Stop terminates a port forward.
|
||||||
Stop()
|
Stop()
|
||||||
|
|
||||||
|
// ID returns the pf id.
|
||||||
|
ID() string
|
||||||
|
|
||||||
// Path returns a resource FQN.
|
// Path returns a resource FQN.
|
||||||
Path() string
|
Path() string
|
||||||
|
|
||||||
// Container returns a container name.
|
// Container returns a container name.
|
||||||
Container() string
|
Container() string
|
||||||
|
|
||||||
// Ports returns container exposed ports.
|
// Ports returns the port mapping.
|
||||||
Ports() []string
|
Port() string
|
||||||
|
|
||||||
// FQN returns the full port-forward name.
|
// FQN returns the full port-forward name.
|
||||||
FQN() string
|
FQN() string
|
||||||
|
|
@ -49,10 +52,11 @@ func NewForwarders() Forwarders {
|
||||||
return make(map[string]Forwarder)
|
return make(map[string]Forwarder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BOZO!! Review!!!
|
||||||
// IsPodForwarded checks if pod has a forward.
|
// IsPodForwarded checks if pod has a forward.
|
||||||
func (ff Forwarders) IsPodForwarded(path string) bool {
|
func (ff Forwarders) IsPodForwarded(path string) bool {
|
||||||
for k := range ff {
|
for k := range ff {
|
||||||
fqn := strings.Split(k, ":")
|
fqn := strings.Split(k, "|")
|
||||||
if fqn[0] == path {
|
if fqn[0] == path {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -78,18 +82,14 @@ func (ff Forwarders) DeleteAll() {
|
||||||
|
|
||||||
// Kill stops and delete a port-forwards associated with pod.
|
// Kill stops and delete a port-forwards associated with pod.
|
||||||
func (ff Forwarders) Kill(path string) int {
|
func (ff Forwarders) Kill(path string) int {
|
||||||
hasContainer := strings.Contains(path, ":")
|
|
||||||
var stats int
|
var stats int
|
||||||
for k, f := range ff {
|
for k, f := range ff {
|
||||||
victim := k
|
victim := k
|
||||||
if !hasContainer {
|
|
||||||
victim = strings.Split(k, ":")[0]
|
|
||||||
}
|
|
||||||
if victim == path {
|
if victim == path {
|
||||||
stats++
|
stats++
|
||||||
log.Debug().Msgf("Stop + Delete port-forward %s", k)
|
log.Debug().Msgf("Stop + Delete port-forward %s", victim)
|
||||||
f.Stop()
|
f.Stop()
|
||||||
delete(ff, k)
|
delete(ff, victim)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
|
|
@ -238,7 +238,7 @@ func makeDoubleCMKeysContainer(n string, optional bool) *v1.Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
func load(t *testing.T, n string) *unstructured.Unstructured {
|
func load(t *testing.T, n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var o unstructured.Unstructured
|
var o unstructured.Unstructured
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue