release v0.25.8 (#1354)

mine
Fernand Galiana 2021-12-03 15:18:12 -07:00 committed by GitHub
parent 265e0561aa
commit f97bceaf64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 326 additions and 177 deletions

View File

@ -5,7 +5,7 @@ PACKAGE := github.com/derailed/$(NAME)
GIT_REV ?= $(shell git rev-parse --short HEAD) GIT_REV ?= $(shell git rev-parse --short HEAD)
SOURCE_DATE_EPOCH ?= $(shell date +%s) SOURCE_DATE_EPOCH ?= $(shell date +%s)
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
VERSION ?= v0.25.7 VERSION ?= v0.25.8
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,26 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.25.8
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!
If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
## Maintenance Release!
---
## Resolved Issues
* [Issue #1349](https://github.com/derailed/k9s/issues/1349) Support events.k8s.io Event v1
* [Issue #1345](https://github.com/derailed/k9s/issues/1345) Access denied after context switch
* [Issue #1344](https://github.com/derailed/k9s/issues/1344) Use "Port forward",but "invalid container port"
* [Issue #1342](https://github.com/derailed/k9s/issues/1342) Log screen refreshed every second
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

2
go.mod
View File

@ -14,7 +14,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v4 v4.1.2 github.com/cenkalti/backoff/v4 v4.1.2
github.com/derailed/popeye v0.9.8 github.com/derailed/popeye v0.9.8
github.com/derailed/tview v0.6.5 github.com/derailed/tview v0.6.6
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.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

4
go.sum
View File

@ -333,8 +333,8 @@ github.com/derailed/popeye v0.9.8 h1:53Rdx09WloOj6ltZZq9OeS48zH0F44mEMcs8XaI1g0Q
github.com/derailed/popeye v0.9.8/go.mod h1:Ih3wTG7wBOuxdqz5tlCuCFq/vyB+Te/IpqY5HwgUTEA= github.com/derailed/popeye v0.9.8/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.5 h1:e377Rv6zYtwtmzDZFmwgkJiiZl/Ax3KIqfx2rlcwWo4= github.com/derailed/tview v0.6.6 h1:hNqBewhRTYRgfLp1p5KGw0DFdbGMS68iocBSmGGNg4s=
github.com/derailed/tview v0.6.5/go.mod h1:A1LXWlbx/YDMXr3GVTy+IgclAkBssJpw/FiZ7aqUgzU= github.com/derailed/tview v0.6.6/go.mod h1:A1LXWlbx/YDMXr3GVTy+IgclAkBssJpw/FiZ7aqUgzU=
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=

View File

@ -82,7 +82,7 @@ func (c *Config) clientConfig() clientcmd.ClientConfig {
} }
func (c *Config) reset() { func (c *Config) reset() {
c.clientCfg = nil c.clientCfg, c.rawCfg = nil, nil
} }
// SwitchContext changes the kubeconfig context to a new cluster. // SwitchContext changes the kubeconfig context to a new cluster.
@ -94,8 +94,10 @@ func (c *Config) SwitchContext(name string) error {
if err != nil { if err != nil {
return fmt.Errorf("context %q does not exist", name) return fmt.Errorf("context %q does not exist", name)
} }
c.flags.Namespace = &context.Namespace
c.flags.Context = &name c.flags.Context = &name
c.flags.ClusterName = &(context.Cluster) c.flags.ClusterName = &(context.Cluster)
c.reset()
return nil return nil
} }
@ -279,16 +281,6 @@ func (c *Config) CurrentNamespaceName() (string, error) {
return ns, err return ns, err
} }
// NamespaceNames fetch all available namespaces on current cluster.
func (c *Config) NamespaceNames(nns []v1.Namespace) []string {
nn := make([]string, 0, len(nns))
for _, ns := range nns {
nn = append(nn, ns.Name)
}
return nn
}
// ConfigAccess return the current kubeconfig api server access configuration. // ConfigAccess return the current kubeconfig api server access configuration.
func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) {
c.mutex.RLock() c.mutex.RLock()
@ -300,6 +292,16 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
// NamespaceNames fetch all available namespaces on current cluster.
func NamespaceNames(nns []v1.Namespace) []string {
nn := make([]string, 0, len(nns))
for _, ns := range nns {
nn = append(nn, ns.Name)
}
return nn
}
func isSet(s *string) bool { func isSet(s *string) bool {
return s != nil && len(*s) != 0 return s != nil && len(*s) != 0
} }

View File

@ -285,20 +285,12 @@ func TestConfigBadConfig(t *testing.T) {
} }
func TestNamespaceNames(t *testing.T) { func TestNamespaceNames(t *testing.T) {
kubeConfig := "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
}
cfg := client.NewConfig(&flags)
nn := []v1.Namespace{ nn := []v1.Namespace{
{ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}},
{ObjectMeta: metav1.ObjectMeta{Name: "ns2"}}, {ObjectMeta: metav1.ObjectMeta{Name: "ns2"}},
} }
nns := cfg.NamespaceNames(nn) nns := client.NamespaceNames(nn)
assert.Equal(t, 2, len(nns)) assert.Equal(t, 2, len(nns))
assert.Equal(t, []string{"ns1", "ns2"}, nns) assert.Equal(t, []string{"ns1", "ns2"}, nns)
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
) )
@ -38,9 +37,6 @@ type (
// ClusterNames() returns all available cluster names. // ClusterNames() returns all available cluster names.
ClusterNames() ([]string, error) ClusterNames() ([]string, error)
// NamespaceNames returns all available namespace names.
NamespaceNames(nn []v1.Namespace) []string
} }
// Config tracks K9s configuration options. // Config tracks K9s configuration options.

View File

@ -32,7 +32,7 @@ func (n *Namespace) Validate(c client.Connection, ks KubeSettings) {
if err != nil { if err != nil {
return return
} }
nn := ks.NamespaceNames(nns) nn := client.NamespaceNames(nns)
if !n.isAllNamespaces() && !InList(nn, n.Active) { if !n.isAllNamespaces() && !InList(nn, n.Active) {
log.Error().Msgf("[Config] Validation error active namespace %q does not exists", n.Active) log.Error().Msgf("[Config] Validation error active namespace %q does not exists", n.Active)
} }

View File

@ -27,14 +27,12 @@ func TestNSValidateMissing(t *testing.T) {
mc := NewMockConnection() mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings() mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2"})
ns := config.NewNamespace() ns := config.NewNamespace()
ns.Validate(mc, mk) ns.Validate(mc, mk)
mk.VerifyWasCalledOnce()
assert.Equal(t, "default", ns.Active) assert.Equal(t, "default", ns.Active)
assert.Equal(t, []string{}, ns.Favorites) assert.Equal(t, []string{"default"}, ns.Favorites)
} }
func TestNSValidateNoNS(t *testing.T) { func TestNSValidateNoNS(t *testing.T) {
@ -78,17 +76,13 @@ func TestNSSetActive(t *testing.T) {
} }
func TestNSValidateRmFavs(t *testing.T) { func TestNSValidateRmFavs(t *testing.T) {
allNS := []string{"default", "kube-system"}
mc := NewMockConnection() mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings() mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn(allNS)
ns := config.NewNamespace() ns := config.NewNamespace()
ns.Favorites = []string{"default", "fred", "blee"} ns.Favorites = []string{"default", "fred", "blee"}
ns.Validate(mc, mk) ns.Validate(mc, mk)
assert.Equal(t, []string{"default"}, ns.Favorites) assert.Equal(t, []string{"default", "fred"}, ns.Favorites)
} }

View File

@ -84,9 +84,6 @@ var Registry = map[string]ResourceMeta{
"v1/endpoints": { "v1/endpoints": {
Renderer: &render.Endpoints{}, Renderer: &render.Endpoints{},
}, },
"v1/events": {
Renderer: &render.Event{},
},
"v1/pods": { "v1/pods": {
DAO: &dao.Pod{}, DAO: &dao.Pod{},
Renderer: &render.Pod{}, Renderer: &render.Pod{},

View File

@ -22,8 +22,8 @@ func (c ContainerPortSpecs) Dump() string {
// InSpecs checks if given port matches a spec. // InSpecs checks if given port matches a spec.
func (c ContainerPortSpecs) MatchSpec(s string) bool { func (c ContainerPortSpecs) MatchSpec(s string) bool {
// No port are exposed // Skip validation if No port are exposed or no container port spec.
if len(c) == 0 { if len(c) == 0 || !strings.Contains(s, "::") {
return true return true
} }
for _, spec := range c { for _, spec := range c {

View File

@ -11,13 +11,16 @@ import (
const ( const (
// K9sAutoPortForwardKey represents an auto portforwards annotation. // K9sAutoPortForwardKey represents an auto portforwards annotation.
K9sAutoPortForwardsKey = "k9scli.io/auto-portforwards" K9sAutoPortForwardsKey = "k9scli.io/auto-port-forwards"
// K9sPortForwardKey represents a portforwards annotation. // K9sPortForwardKey represents a portforwards annotation.
K9sPortForwardsKey = "k9scli.io/portforwards" K9sPortForwardsKey = "k9scli.io/port-forwards"
) )
var pfRX = regexp.MustCompile(`\A([\w-]+)::(\d*):?(\d*|[\w-]*)/?(\d+)?\z`) var (
pfRX = regexp.MustCompile(`\A([\w-]+)::(\d*):?(\d*|[\w-]*)/?(\d+)?\z`)
pfPlainRX = regexp.MustCompile(`\A(\d*):?(\d*|[\w-]*)\z`)
)
// PFAnn represents a portforward annotation value. // PFAnn represents a portforward annotation value.
// Shape: container/portname|portNum:localPort // Shape: container/portname|portNum:localPort
@ -28,12 +31,37 @@ type PFAnn struct {
containerPortNum string containerPortNum string
} }
func ParsePlainPF(ann string) (*PFAnn, error) {
if len(ann) == 0 {
return nil, fmt.Errorf("invalid annotation %q", ann)
}
var pf PFAnn
mm := pfPlainRX.FindStringSubmatch(strings.TrimSpace(ann))
if len(mm) < 3 {
return nil, fmt.Errorf("Invalid plain port-forward %s", ann)
}
if len(mm[2]) == 0 {
pf.ContainerPort = intstr.Parse(mm[1])
pf.LocalPort = mm[1]
return &pf, nil
}
pf.LocalPort, pf.ContainerPort = mm[1], intstr.Parse(mm[2])
return &pf, nil
}
// ParsePF hydrate a portforward annotation from string. // ParsePF hydrate a portforward annotation from string.
func ParsePF(ann string) (*PFAnn, error) { func ParsePF(ann string) (*PFAnn, error) {
if pf, err := ParsePlainPF(ann); err == nil {
return pf, nil
}
var pf PFAnn var pf PFAnn
if mm := pfPlainRX.FindStringSubmatch(strings.TrimSpace(ann)); len(mm) == 3 {
pf.containerPortNum = mm[0]
}
r := pfRX.FindStringSubmatch(strings.TrimSpace(ann)) r := pfRX.FindStringSubmatch(strings.TrimSpace(ann))
if len(r) < 4 { if len(r) < 4 {
return &pf, fmt.Errorf("invalid pf annotation %s", ann) return &pf, fmt.Errorf("invalid port-forward specification %s", ann)
} }
pf.Container = r[1] pf.Container = r[1]
pf.LocalPort, pf.ContainerPort = r[2], intstr.Parse(r[3]) pf.LocalPort, pf.ContainerPort = r[2], intstr.Parse(r[3])

View File

@ -41,9 +41,21 @@ func TestParsePF(t *testing.T) {
containerPort: intstr.Parse("1234"), containerPort: intstr.Parse("1234"),
localPort: "1234", localPort: "1234",
}, },
"plain-single": {
exp: "1234",
container: "",
containerPort: intstr.Parse("1234"),
localPort: "1234",
},
"plain-full": {
exp: "4321:1234",
container: "",
containerPort: intstr.Parse("1234"),
localPort: "4321",
},
"toast": { "toast": {
exp: "c1:4321:1234", exp: "c1:4321:1234",
e: errors.New("invalid pf annotation c1:4321:1234"), e: errors.New("invalid port-forward specification c1:4321:1234"),
}, },
} }

View File

@ -32,9 +32,6 @@ func (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (string, string) {
func (aa PFAnns) ToTunnels(address string, pp ContainerPortSpecs, available PortChecker) (PortTunnels, error) { func (aa PFAnns) ToTunnels(address string, pp ContainerPortSpecs, available PortChecker) (PortTunnels, error) {
pts := make(PortTunnels, 0, len(aa)) pts := make(PortTunnels, 0, len(aa))
for _, a := range 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) pt, err := a.ToTunnel(address)
if err != nil { if err != nil {
return pts, err return pts, err

View File

@ -11,40 +11,47 @@ import (
func TestParsePFs(t *testing.T) { func TestParsePFs(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
exp string spec string
pfs port.PFAnns pfs port.PFAnns
e error e error
}{ }{
"single": { "single": {
exp: "c2::4321:1234", spec: "c2::4321:1234",
pfs: port.PFAnns{ pfs: port.PFAnns{
{Container: "c2", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, {Container: "c2", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"},
}, },
}, },
"multi": { "multi": {
exp: "c1::4321:1234,c2::6666:6543", spec: "c1::4321:1234,c2::6666:6543",
pfs: port.PFAnns{ pfs: port.PFAnns{
{Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, {Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"},
{Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"}, {Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"},
}, },
}, },
"spaces": { "spaces": {
exp: " c1::4321:1234 , c2::6666:6543 ", spec: " c1::4321:1234 , c2::6666:6543 ",
pfs: port.PFAnns{ pfs: port.PFAnns{
{Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, {Container: "c1", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"},
{Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"}, {Container: "c2", ContainerPort: intstr.Parse("6543"), LocalPort: "6666"},
}, },
}, },
"plain-multi": {
spec: "4321:1234, 6666:6543",
pfs: port.PFAnns{
{ContainerPort: intstr.Parse("1234"), LocalPort: "4321"},
{ContainerPort: intstr.Parse("6543"), LocalPort: "6666"},
},
},
"toast": { "toast": {
exp: "c1::p1:1234,c2::4321", spec: "c1::p1:1234,c2::4321",
e: errors.New("invalid pf annotation c1::p1:1234"), e: errors.New("invalid port-forward specification c1::p1:1234"),
}, },
} }
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) {
pfs, err := port.ParsePFs(u.exp) pfs, err := port.ParsePFs(u.spec)
assert.Equal(t, u.e, err) assert.Equal(t, u.e, err)
if err != nil { if err != nil {
return return
@ -78,7 +85,7 @@ func TestPFsToTunnel(t *testing.T) {
pts: port.PortTunnels{ pts: port.PortTunnels{
{Address: "fred", Container: "c2", ContainerPort: "1234", LocalPort: "4321"}, {Address: "fred", Container: "c2", ContainerPort: "1234", LocalPort: "4321"},
}, },
e: errors.New("ann does not match container port specs"), e: errors.New("no port number assigned"),
}, },
} }

View File

@ -1,19 +1,30 @@
package render package render
import ( import (
"errors"
"fmt"
"strconv"
"strings" "strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
) )
// BOZO!!
// import (
// "errors"
// "fmt"
// "strconv"
// "strings"
// "time"
// "github.com/derailed/k9s/internal/client"
// "github.com/derailed/tview"
// "github.com/gdamore/tcell/v2"
// v1 "k8s.io/api/core/v1"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
// "k8s.io/apimachinery/pkg/runtime"
// "k8s.io/apimachinery/pkg/util/duration"
// api "k8s.io/kubernetes/pkg/apis/core"
// )
// Event renders a K8s Event to screen. // Event renders a K8s Event to screen.
type Event struct{} type Event struct{}
@ -35,59 +46,128 @@ func (e Event) ColorerFunc() ColorerFunc {
} }
} }
// Header returns a header rbw. // // Header returns a header rbw.
func (Event) Header(ns string) Header { // func (Event) Header(ns string) Header {
return Header{ // return Header{
HeaderColumn{Name: "NAMESPACE"}, // HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"}, // HeaderColumn{Name: "LAST SEEN"},
HeaderColumn{Name: "TYPE"}, // HeaderColumn{Name: "TYPE"},
HeaderColumn{Name: "REASON"}, // HeaderColumn{Name: "REASON"},
HeaderColumn{Name: "SOURCE"}, // HeaderColumn{Name: "OBJECT"},
HeaderColumn{Name: "COUNT", Align: tview.AlignRight}, // HeaderColumn{Name: "SUBOBJECT"},
HeaderColumn{Name: "MESSAGE", Wide: true}, // HeaderColumn{Name: "SOURCE"},
HeaderColumn{Name: "VALID", Wide: true}, // HeaderColumn{Name: "MESSAGE", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator}, // HeaderColumn{Name: "FIRST SEEN", Wide: true},
} // HeaderColumn{Name: "COUNT", Align: tview.AlignRight},
} // HeaderColumn{Name: "NAME"},
// HeaderColumn{Name: "VALID", Wide: true},
// }
// }
// Render renders a K8s resource to screen. // // Render renders a K8s resource to screen.
func (e Event) Render(o interface{}, ns string, r *Row) error { // func (e Event) Render(o interface{}, ns string, r *Row) error {
raw, ok := o.(*unstructured.Unstructured) // raw, ok := o.(*unstructured.Unstructured)
if !ok { // if !ok {
return fmt.Errorf("Expected Event, but got %T", o) // return fmt.Errorf("Expected Event, but got %T", o)
} // }
var ev v1.Event // var ev api.Event
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ev) // err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ev)
if err != nil { // if err != nil {
return err // return err
} // }
r.ID = client.MetaFQN(ev.ObjectMeta) // firstTimestamp := translateTimestampSince(ev.FirstTimestamp)
r.Fields = Fields{ // if ev.FirstTimestamp.IsZero() {
ev.Namespace, // firstTimestamp = translateMicroTimestampSince(ev.EventTime)
asRef(ev.InvolvedObject), // }
ev.Type,
ev.Reason,
ev.Source.Component,
strconv.Itoa(int(ev.Count)),
ev.Message,
asStatus(e.diagnose(ev.Type)),
toAge(ev.LastTimestamp),
}
return nil // lastTimestamp := translateTimestampSince(ev.LastTimestamp)
} // if ev.LastTimestamp.IsZero() {
// lastTimestamp = firstTimestamp
// }
// count := ev.Count
// if ev.Series != nil {
// lastTimestamp = translateMicroTimestampSince(ev.Series.LastObservedTime)
// count = ev.Series.Count
// } else if count == 0 {
// // Singleton events don't have a count set in the new API.
// count = 1
// }
// Happy returns true if resource is happy, false otherwise. // var target string
func (Event) diagnose(kind string) error { // if len(ev.InvolvedObject.Name) > 0 {
if kind != "Normal" { // target = fmt.Sprintf("%s/%s", strings.ToLower(ev.InvolvedObject.Kind), ev.InvolvedObject.Name)
return errors.New("failed event") // } else {
} // target = strings.ToLower(ev.InvolvedObject.Kind)
return nil // }
}
// Helpers... // r.ID = client.MetaFQN(ev.ObjectMeta)
// r.Fields = Fields{
// ev.Namespace,
// lastTimestamp,
// ev.Type,
// ev.Reason,
// target,
// ev.InvolvedObject.FieldPath,
// fmtEventSource(ev.Source, ev.ReportingController, ev.ReportingInstance),
// strings.TrimSpace(ev.Message),
// firstTimestamp,
// strconv.Itoa(int(count)),
// ev.Name,
// asStatus(e.diagnose(ev.Type)),
// }
func asRef(r v1.ObjectReference) string { // return nil
return strings.ToLower(r.Kind) + ":" + r.Name // }
}
// func translateMicroTimestampSince(timestamp metav1.MicroTime) string {
// if timestamp.IsZero() {
// return "<unknown>"
// }
// return duration.HumanDuration(time.Since(timestamp.Time))
// }
// func translateTimestampSince(timestamp metav1.Time) string {
// if timestamp.IsZero() {
// return "<unknown>"
// }
// return duration.HumanDuration(time.Since(timestamp.Time))
// }
// func fmtEventSource(es api.EventSource, reportingController, reportingInstance string) string {
// return fmtEventSourceComponentInstance(
// firstNonEmpty(es.Component, reportingController),
// firstNonEmpty(es.Host, reportingInstance),
// )
// }
// func fmtEventSourceComponentInstance(component, instance string) string {
// if len(instance) == 0 {
// return component
// }
// return component + ", " + instance
// }
// func firstNonEmpty(ss ...string) string {
// for _, s := range ss {
// if len(s) > 0 {
// return s
// }
// }
// return ""
// }
// // Happy returns true if resource is happy, false otherwise.
// func (Event) diagnose(kind string) error {
// if kind != "Normal" {
// return errors.New("failed event")
// }
// return nil
// }
// // Helpers...
// func asRef(r v1.ObjectReference) string {
// return strings.ToLower(r.Kind) + ":" + r.Name
// }

View File

@ -1,29 +1,23 @@
package render_test package render_test
import ( // BOZO!!
"testing" // func TestEventRender(t *testing.T) {
// c := render.Event{}
// r := render.NewRow(7)
// c.Render(load(t, "ev"), "", &r)
"github.com/derailed/k9s/internal/render" // assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID)
"github.com/stretchr/testify/assert" // assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7])
) // }
func TestEventRender(t *testing.T) { // func BenchmarkEventRender(b *testing.B) {
c := render.Event{} // ev := load(b, "ev")
r := render.NewRow(7) // var re render.Event
c.Render(load(t, "ev"), "", &r) // r := render.NewRow(7)
assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID) // b.ResetTimer()
assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7]) // b.ReportAllocs()
} // for i := 0; i < b.N; i++ {
// _ = re.Render(&ev, "", &r)
func BenchmarkEventRender(b *testing.B) { // }
ev := load(b, "ev") // }
var re render.Event
r := render.NewRow(7)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = re.Render(&ev, "", &r)
}
}

View File

@ -61,16 +61,15 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
if !ok { if !ok {
return fmt.Errorf("expecting a TableRow but got %T", o) return fmt.Errorf("expecting a TableRow but got %T", o)
} }
nns, err := resourceNS(row.Object.Raw) nns, name, err := resourceNS(row.Object.Raw)
if err != nil { if err != nil {
return err return err
} }
n, ok := row.Cells[0].(string)
if !ok { if !ok {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0]) return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
} }
r.ID = client.FQN(nns, n) r.ID = client.FQN(nns, name)
r.Fields = make(Fields, 0, len(g.Header(ns))) r.Fields = make(Fields, 0, len(g.Header(ns)))
r.Fields = append(r.Fields, nns) r.Fields = append(r.Fields, nns)
var ageCell interface{} var ageCell interface{}
@ -95,26 +94,35 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func resourceNS(raw []byte) (string, error) { func resourceNS(raw []byte) (string, string, error) {
var obj map[string]interface{} var obj map[string]interface{}
var ns, name string
err := json.Unmarshal(raw, &obj) err := json.Unmarshal(raw, &obj)
if err != nil { if err != nil {
return "", err return ns, name, err
} }
meta, ok := obj["metadata"].(map[string]interface{}) meta, ok := obj["metadata"].(map[string]interface{})
if !ok { if !ok {
return "", errors.New("no metadata found on generic resource") return ns, name, errors.New("no metadata found on generic resource")
}
ina, ok := meta["name"]
if !ok {
return ns, name, errors.New("unable to extract resource name")
}
name, ok = ina.(string)
if !ok {
return ns, name, fmt.Errorf("expecting name string type but got %T", ns)
} }
ns, ok := meta["namespace"] ins, ok := meta["namespace"]
if !ok { if !ok {
return client.ClusterScope, nil return client.ClusterScope, name, nil
} }
nns, ok := ns.(string) ns, ok = ins.(string)
if !ok { if !ok {
return "", fmt.Errorf("expecting namespace string type but got %T", ns) return ns, name, fmt.Errorf("expecting namespace string type but got %T", ns)
} }
return nns, nil return ns, name, nil
} }

View File

@ -21,7 +21,7 @@ func TestGenericRender(t *testing.T) {
"withNS": { "withNS": {
ns: "ns1", ns: "ns1",
table: makeNSGeneric(), table: makeNSGeneric(),
eID: "ns1/c1", eID: "ns1/fred",
eFields: render.Fields{"ns1", "c1", "c2", "c3"}, eFields: render.Fields{"ns1", "c1", "c2", "c3"},
eHeader: render.Header{ eHeader: render.Header{
render.HeaderColumn{Name: "NAMESPACE"}, render.HeaderColumn{Name: "NAMESPACE"},
@ -33,7 +33,7 @@ func TestGenericRender(t *testing.T) {
"all": { "all": {
ns: client.NamespaceAll, ns: client.NamespaceAll,
table: makeNSGeneric(), table: makeNSGeneric(),
eID: "ns1/c1", eID: "ns1/fred",
eFields: render.Fields{"ns1", "c1", "c2", "c3"}, eFields: render.Fields{"ns1", "c1", "c2", "c3"},
eHeader: render.Header{ eHeader: render.Header{
render.HeaderColumn{Name: "NAMESPACE"}, render.HeaderColumn{Name: "NAMESPACE"},
@ -45,7 +45,7 @@ func TestGenericRender(t *testing.T) {
"allNS": { "allNS": {
ns: client.AllNamespaces, ns: client.AllNamespaces,
table: makeNSGeneric(), table: makeNSGeneric(),
eID: "ns1/c1", eID: "ns1/fred",
eFields: render.Fields{"ns1", "c1", "c2", "c3"}, eFields: render.Fields{"ns1", "c1", "c2", "c3"},
eHeader: render.Header{ eHeader: render.Header{
render.HeaderColumn{Name: "NAMESPACE"}, render.HeaderColumn{Name: "NAMESPACE"},
@ -57,7 +57,7 @@ func TestGenericRender(t *testing.T) {
"clusterWide": { "clusterWide": {
ns: client.ClusterScope, ns: client.ClusterScope,
table: makeNoNSGeneric(), table: makeNoNSGeneric(),
eID: "-/c1", eID: "-/fred",
eFields: render.Fields{"-", "c1", "c2", "c3"}, eFields: render.Fields{"-", "c1", "c2", "c3"},
eHeader: render.Header{ eHeader: render.Header{
render.HeaderColumn{Name: "NAMESPACE"}, render.HeaderColumn{Name: "NAMESPACE"},
@ -69,7 +69,7 @@ func TestGenericRender(t *testing.T) {
"age": { "age": {
ns: client.ClusterScope, ns: client.ClusterScope,
table: makeAgeGeneric(), table: makeAgeGeneric(),
eID: "-/c1", eID: "-/fred",
eFields: render.Fields{"-", "c1", "c2", "Age"}, eFields: render.Fields{"-", "c1", "c2", "Age"},
eHeader: render.Header{ eHeader: render.Header{
render.HeaderColumn{Name: "NAMESPACE"}, render.HeaderColumn{Name: "NAMESPACE"},

View File

@ -19,7 +19,7 @@ func NewEvent(gvr client.GVR) ResourceViewer {
} }
e.GetTable().SetColorerFn(render.Event{}.ColorerFunc()) e.GetTable().SetColorerFn(render.Event{}.ColorerFunc())
e.AddBindKeysFn(e.bindKeys) e.AddBindKeysFn(e.bindKeys)
e.GetTable().SetSortCol(ageCol, true) e.GetTable().SetSortCol("LAST SEEN", false)
return &e return &e
} }
@ -27,9 +27,10 @@ func NewEvent(gvr client.GVR) ResourceViewer {
func (e *Event) bindKeys(aa ui.KeyActions) { func (e *Event) bindKeys(aa ui.KeyActions) {
aa.Delete(tcell.KeyCtrlD, ui.KeyE) aa.Delete(tcell.KeyCtrlD, ui.KeyE)
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
ui.KeyShiftY: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd("TYPE", true), false), ui.KeyShiftL: ui.NewKeyAction("Sort LastSeen", e.GetTable().SortColCmd("LAST SEEN", false), false),
ui.KeyShiftT: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd("TYPE", true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Reason", e.GetTable().SortColCmd("REASON", true), false), ui.KeyShiftR: ui.NewKeyAction("Sort Reason", e.GetTable().SortColCmd("REASON", true), false),
ui.KeyShiftE: ui.NewKeyAction("Sort Source", e.GetTable().SortColCmd("SOURCE", true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Source", e.GetTable().SortColCmd("SOURCE", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Count", e.GetTable().SortColCmd("COUNT", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Count", e.GetTable().SortColCmd("COUNT", true), false),
}) })
} }

View File

@ -3,6 +3,7 @@ package view
import ( import (
"fmt" "fmt"
"math" "math"
"strconv"
"strings" "strings"
"github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/port"
@ -39,20 +40,20 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe
p1, p2 := pf.ToPortSpec(ports) 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, nil) f.AddInputField("Container Port:", p1, fieldLen, nil, nil)
f.AddInputField("Local Port:", p2, fieldLen, nil, nil)
coField := f.GetFormItemByLabel("Container Port:").(*tview.InputField) coField := f.GetFormItemByLabel("Container Port:").(*tview.InputField)
loField := f.GetFormItemByLabel("Local Port:").(*tview.InputField)
if coField.GetText() == "" { if coField.GetText() == "" {
coField.SetPlaceholder("Enter a container name::port") coField.SetPlaceholder("Enter a container name::port")
} }
f.AddInputField("Local Port:", p2, fieldLen, nil, nil)
loField := f.GetFormItemByLabel("Local Port:").(*tview.InputField)
if loField.GetText() == "" {
loField.SetPlaceholder("Enter a local port")
}
coField.SetChangedFunc(func(s string) { coField.SetChangedFunc(func(s string) {
port := extractPort(s) port := extractPort(s)
loField.SetText(port) loField.SetText(port)
p2 = port p2 = port
}) })
if loField.GetText() == "" {
loField.SetPlaceholder("Enter a local port")
}
f.AddInputField("Address:", address, fieldLen, nil, func(h string) { f.AddInputField("Address:", address, fieldLen, nil, func(h string) {
address = h address = h
}) })
@ -70,10 +71,6 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe
v.App().Flash().Err(fmt.Errorf("container to local port mismatch")) v.App().Flash().Err(fmt.Errorf("container to local port mismatch"))
return return
} }
if !ports.MatchSpec(coField.GetText()) {
v.App().Flash().Err(fmt.Errorf("invalid container port"))
return
}
tt, err := port.ToTunnels(address, coField.GetText(), loField.GetText()) tt, err := port.ToTunnels(address, coField.GetText(), loField.GetText())
if err != nil { if err != nil {
v.App().Flash().Err(err) v.App().Flash().Err(err)
@ -122,10 +119,16 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func extractPort(coPort string) string { func extractPort(port string) string {
tokens := strings.Split(coPort, "::") tokens := strings.Split(port, "::")
if len(tokens) < 2 { if len(tokens) < 2 {
return "" ports := strings.Split(port, ",")
for _, t := range ports {
if _, err := strconv.Atoi(strings.TrimSpace(t)); err != nil {
return ""
}
}
return port
} }
return tokens[1] return tokens[1]

View File

@ -2,6 +2,7 @@ package view
import ( import (
"fmt" "fmt"
"strings"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/port"
@ -41,16 +42,21 @@ func (p *PortForwardExtender) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
pod, err := p.fetchPodName(path) p.fetchPodName(path)
pod, err := fetchPod(p.App().factory, path)
if err != nil { if err != nil {
p.App().Flash().Err(err) p.App().Flash().Err(err)
return nil return nil
} }
if p.App().factory.Forwarders().IsPodForwarded(pod) { if pod.Status.Phase != v1.PodRunning {
p.App().Flash().Errf("pod must be running. Current status=%v", pod.Status.Phase)
return nil
}
if p.App().factory.Forwarders().IsPodForwarded(path) {
p.App().Flash().Errf("A PortForward already exist for pod %s", pod) p.App().Flash().Errf("A PortForward already exist for pod %s", pod)
return nil return nil
} }
if err := showFwdDialog(p, pod, startFwdCB); err != nil { if err := showFwdDialog(p, path, startFwdCB); err != nil {
p.App().Flash().Err(err) p.App().Flash().Err(err)
} }
@ -77,7 +83,6 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward
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", pf.ID())
DismissPortForwards(v, v.App().Content.Pages) DismissPortForwards(v, v.App().Content.Pages)
}) })
@ -98,6 +103,7 @@ func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error {
return err return err
} }
tt := make([]string, 0, len(pts))
for _, pt := range pts { for _, pt := range pts {
if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, pt.Container, pt.PortMap())); ok { 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) return fmt.Errorf("A port-forward is already active on pod %s", path)
@ -109,7 +115,13 @@ func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error {
} }
log.Debug().Msgf(">>> Starting port forward %q -- %#v", pf.ID(), pt) log.Debug().Msgf(">>> Starting port forward %q -- %#v", pf.ID(), pt)
go runForward(v, pf, fwd) go runForward(v, pf, fwd)
tt = append(tt, pt.ContainerPort)
} }
if len(tt) == 1 {
v.App().Flash().Infof("PortForward activated %s", tt[0])
return nil
}
v.App().Flash().Infof("PortForwards activated %s", strings.Join(tt, ","))
return nil return nil
} }