diff --git a/Makefile b/Makefile index 78647fdd..7b7116a6 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PACKAGE := github.com/derailed/$(NAME) GIT_REV ?= $(shell git rev-parse --short HEAD) SOURCE_DATE_EPOCH ?= $(shell date +%s) 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 IMAGE := ${IMG_NAME}:${VERSION} diff --git a/change_logs/release_v0.25.8.md b/change_logs/release_v0.25.8.md new file mode 100644 index 00000000..4b6a55c4 --- /dev/null +++ b/change_logs/release_v0.25.8.md @@ -0,0 +1,26 @@ + + +# 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 + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/go.mod b/go.mod index 32f78599..38440a38 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.1.2 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/fsnotify/fsnotify v1.5.1 github.com/fvbommel/sortorder v1.0.2 diff --git a/go.sum b/go.sum index dd5d0a51..bd0f63ba 100644 --- a/go.sum +++ b/go.sum @@ -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/tcell/v2 v2.3.1-rc.2 h1:9TmZB/IwL3MA1Jf4pC4rfMaPTcVYIN62IwE7X7A9emU= github.com/derailed/tcell/v2 v2.3.1-rc.2/go.mod h1:wegJ+SscH+jPjEQIAV/dI/grLTRm5R4IE2M479NDSL0= -github.com/derailed/tview v0.6.5 h1:e377Rv6zYtwtmzDZFmwgkJiiZl/Ax3KIqfx2rlcwWo4= -github.com/derailed/tview v0.6.5/go.mod h1:A1LXWlbx/YDMXr3GVTy+IgclAkBssJpw/FiZ7aqUgzU= +github.com/derailed/tview v0.6.6 h1:hNqBewhRTYRgfLp1p5KGw0DFdbGMS68iocBSmGGNg4s= +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 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= diff --git a/internal/client/config.go b/internal/client/config.go index e3088871..33ea1655 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -82,7 +82,7 @@ func (c *Config) clientConfig() clientcmd.ClientConfig { } func (c *Config) reset() { - c.clientCfg = nil + c.clientCfg, c.rawCfg = nil, nil } // SwitchContext changes the kubeconfig context to a new cluster. @@ -94,8 +94,10 @@ func (c *Config) SwitchContext(name string) error { if err != nil { return fmt.Errorf("context %q does not exist", name) } + c.flags.Namespace = &context.Namespace c.flags.Context = &name c.flags.ClusterName = &(context.Cluster) + c.reset() return nil } @@ -279,16 +281,6 @@ func (c *Config) CurrentNamespaceName() (string, error) { 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. func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { c.mutex.RLock() @@ -300,6 +292,16 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { // ---------------------------------------------------------------------------- // 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 { return s != nil && len(*s) != 0 } diff --git a/internal/client/config_test.go b/internal/client/config_test.go index bb00208c..eb02cc53 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -285,20 +285,12 @@ func TestConfigBadConfig(t *testing.T) { } func TestNamespaceNames(t *testing.T) { - kubeConfig := "./testdata/config" - - flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - } - - cfg := client.NewConfig(&flags) - nn := []v1.Namespace{ {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, {ObjectMeta: metav1.ObjectMeta{Name: "ns2"}}, } - nns := cfg.NamespaceNames(nn) + nns := client.NamespaceNames(nn) assert.Equal(t, 2, len(nns)) assert.Equal(t, []string{"ns1", "ns2"}, nns) } diff --git a/internal/config/config.go b/internal/config/config.go index 33c8d5d3..70ced92f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,7 +10,6 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" - v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -38,9 +37,6 @@ type ( // ClusterNames() returns all available cluster names. ClusterNames() ([]string, error) - - // NamespaceNames returns all available namespace names. - NamespaceNames(nn []v1.Namespace) []string } // Config tracks K9s configuration options. diff --git a/internal/config/ns.go b/internal/config/ns.go index b3d1b3e7..0cde6e2b 100644 --- a/internal/config/ns.go +++ b/internal/config/ns.go @@ -32,7 +32,7 @@ func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { if err != nil { return } - nn := ks.NamespaceNames(nns) + nn := client.NamespaceNames(nns) if !n.isAllNamespaces() && !InList(nn, n.Active) { log.Error().Msgf("[Config] Validation error active namespace %q does not exists", n.Active) } diff --git a/internal/config/ns_test.go b/internal/config/ns_test.go index a99be719..33489366 100644 --- a/internal/config/ns_test.go +++ b/internal/config/ns_test.go @@ -27,14 +27,12 @@ func TestNSValidateMissing(t *testing.T) { mc := NewMockConnection() m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2"}) ns := config.NewNamespace() ns.Validate(mc, mk) - mk.VerifyWasCalledOnce() assert.Equal(t, "default", ns.Active) - assert.Equal(t, []string{}, ns.Favorites) + assert.Equal(t, []string{"default"}, ns.Favorites) } func TestNSValidateNoNS(t *testing.T) { @@ -78,17 +76,13 @@ func TestNSSetActive(t *testing.T) { } func TestNSValidateRmFavs(t *testing.T) { - allNS := []string{"default", "kube-system"} - mc := NewMockConnection() m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn(allNS) ns := config.NewNamespace() ns.Favorites = []string{"default", "fred", "blee"} ns.Validate(mc, mk) - assert.Equal(t, []string{"default"}, ns.Favorites) + assert.Equal(t, []string{"default", "fred"}, ns.Favorites) } diff --git a/internal/model/registry.go b/internal/model/registry.go index 8bda088c..912733ff 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -84,9 +84,6 @@ var Registry = map[string]ResourceMeta{ "v1/endpoints": { Renderer: &render.Endpoints{}, }, - "v1/events": { - Renderer: &render.Event{}, - }, "v1/pods": { DAO: &dao.Pod{}, Renderer: &render.Pod{}, diff --git a/internal/port/co_portspec.go b/internal/port/co_portspec.go index ab24acc4..13c8ef81 100644 --- a/internal/port/co_portspec.go +++ b/internal/port/co_portspec.go @@ -22,8 +22,8 @@ func (c ContainerPortSpecs) Dump() string { // InSpecs checks if given port matches a spec. func (c ContainerPortSpecs) MatchSpec(s string) bool { - // No port are exposed - if len(c) == 0 { + // Skip validation if No port are exposed or no container port spec. + if len(c) == 0 || !strings.Contains(s, "::") { return true } for _, spec := range c { diff --git a/internal/port/pf.go b/internal/port/pf.go index 96ac3d3f..fb4da570 100644 --- a/internal/port/pf.go +++ b/internal/port/pf.go @@ -11,13 +11,16 @@ import ( const ( // K9sAutoPortForwardKey represents an auto portforwards annotation. - K9sAutoPortForwardsKey = "k9scli.io/auto-portforwards" + K9sAutoPortForwardsKey = "k9scli.io/auto-port-forwards" // 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. // Shape: container/portname|portNum:localPort @@ -28,12 +31,37 @@ type PFAnn struct { 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. func ParsePF(ann string) (*PFAnn, error) { + if pf, err := ParsePlainPF(ann); err == nil { + return pf, nil + } var pf PFAnn + if mm := pfPlainRX.FindStringSubmatch(strings.TrimSpace(ann)); len(mm) == 3 { + pf.containerPortNum = mm[0] + } r := pfRX.FindStringSubmatch(strings.TrimSpace(ann)) 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.LocalPort, pf.ContainerPort = r[2], intstr.Parse(r[3]) diff --git a/internal/port/pf_test.go b/internal/port/pf_test.go index 0505af17..62b6bf99 100644 --- a/internal/port/pf_test.go +++ b/internal/port/pf_test.go @@ -41,9 +41,21 @@ func TestParsePF(t *testing.T) { containerPort: intstr.Parse("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": { exp: "c1:4321:1234", - e: errors.New("invalid pf annotation c1:4321:1234"), + e: errors.New("invalid port-forward specification c1:4321:1234"), }, } diff --git a/internal/port/pfs.go b/internal/port/pfs.go index 70509d01..10cc6bd0 100644 --- a/internal/port/pfs.go +++ b/internal/port/pfs.go @@ -32,9 +32,6 @@ func (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (string, string) { 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 diff --git a/internal/port/pfs_test.go b/internal/port/pfs_test.go index 200ed451..7b60d866 100644 --- a/internal/port/pfs_test.go +++ b/internal/port/pfs_test.go @@ -11,40 +11,47 @@ import ( func TestParsePFs(t *testing.T) { uu := map[string]struct { - exp string - pfs port.PFAnns - e error + spec string + pfs port.PFAnns + e error }{ "single": { - exp: "c2::4321:1234", + spec: "c2::4321:1234", pfs: port.PFAnns{ {Container: "c2", ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, }, }, "multi": { - exp: "c1::4321:1234,c2::6666:6543", + spec: "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 ", + spec: " 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"}, }, }, + "plain-multi": { + spec: "4321:1234, 6666:6543", + pfs: port.PFAnns{ + {ContainerPort: intstr.Parse("1234"), LocalPort: "4321"}, + {ContainerPort: intstr.Parse("6543"), LocalPort: "6666"}, + }, + }, "toast": { - exp: "c1::p1:1234,c2::4321", - e: errors.New("invalid pf annotation c1::p1:1234"), + spec: "c1::p1:1234,c2::4321", + e: errors.New("invalid port-forward specification c1::p1:1234"), }, } for k := range uu { u := uu[k] 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) if err != nil { return @@ -78,7 +85,7 @@ func TestPFsToTunnel(t *testing.T) { pts: port.PortTunnels{ {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"), }, } diff --git a/internal/render/ev.go b/internal/render/ev.go index 9794737d..33bb8a75 100644 --- a/internal/render/ev.go +++ b/internal/render/ev.go @@ -1,19 +1,30 @@ package render import ( - "errors" - "fmt" - "strconv" "strings" - "github.com/derailed/k9s/internal/client" - "github.com/derailed/tview" "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. type Event struct{} @@ -35,59 +46,128 @@ func (e Event) ColorerFunc() ColorerFunc { } } -// Header returns a header rbw. -func (Event) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "TYPE"}, - HeaderColumn{Name: "REASON"}, - HeaderColumn{Name: "SOURCE"}, - HeaderColumn{Name: "COUNT", Align: tview.AlignRight}, - HeaderColumn{Name: "MESSAGE", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator}, - } -} +// // Header returns a header rbw. +// func (Event) Header(ns string) Header { +// return Header{ +// HeaderColumn{Name: "NAMESPACE"}, +// HeaderColumn{Name: "LAST SEEN"}, +// HeaderColumn{Name: "TYPE"}, +// HeaderColumn{Name: "REASON"}, +// HeaderColumn{Name: "OBJECT"}, +// HeaderColumn{Name: "SUBOBJECT"}, +// HeaderColumn{Name: "SOURCE"}, +// HeaderColumn{Name: "MESSAGE", Wide: true}, +// 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. -func (e Event) Render(o interface{}, ns string, r *Row) error { - raw, ok := o.(*unstructured.Unstructured) - if !ok { - return fmt.Errorf("Expected Event, but got %T", o) - } - var ev v1.Event - err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ev) - if err != nil { - return err - } +// // Render renders a K8s resource to screen. +// func (e Event) Render(o interface{}, ns string, r *Row) error { +// raw, ok := o.(*unstructured.Unstructured) +// if !ok { +// return fmt.Errorf("Expected Event, but got %T", o) +// } +// var ev api.Event +// err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ev) +// if err != nil { +// return err +// } - r.ID = client.MetaFQN(ev.ObjectMeta) - r.Fields = Fields{ - ev.Namespace, - 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), - } +// firstTimestamp := translateTimestampSince(ev.FirstTimestamp) +// if ev.FirstTimestamp.IsZero() { +// firstTimestamp = translateMicroTimestampSince(ev.EventTime) +// } - 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. -func (Event) diagnose(kind string) error { - if kind != "Normal" { - return errors.New("failed event") - } - return nil -} +// var target string +// if len(ev.InvolvedObject.Name) > 0 { +// target = fmt.Sprintf("%s/%s", strings.ToLower(ev.InvolvedObject.Kind), ev.InvolvedObject.Name) +// } else { +// target = strings.ToLower(ev.InvolvedObject.Kind) +// } -// 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 strings.ToLower(r.Kind) + ":" + r.Name -} +// return nil +// } + +// func translateMicroTimestampSince(timestamp metav1.MicroTime) string { +// if timestamp.IsZero() { +// return "" +// } + +// return duration.HumanDuration(time.Since(timestamp.Time)) +// } + +// func translateTimestampSince(timestamp metav1.Time) string { +// if timestamp.IsZero() { +// return "" +// } + +// 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 +// } diff --git a/internal/render/ev_test.go b/internal/render/ev_test.go index 9388969e..e65137a1 100644 --- a/internal/render/ev_test.go +++ b/internal/render/ev_test.go @@ -1,29 +1,23 @@ package render_test -import ( - "testing" +// BOZO!! +// func TestEventRender(t *testing.T) { +// c := render.Event{} +// r := render.NewRow(7) +// c.Render(load(t, "ev"), "", &r) - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) +// assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID) +// 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) { - c := render.Event{} - r := render.NewRow(7) - c.Render(load(t, "ev"), "", &r) +// func BenchmarkEventRender(b *testing.B) { +// ev := load(b, "ev") +// var re render.Event +// r := render.NewRow(7) - assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID) - assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7]) -} - -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) - } -} +// b.ResetTimer() +// b.ReportAllocs() +// for i := 0; i < b.N; i++ { +// _ = re.Render(&ev, "", &r) +// } +// } diff --git a/internal/render/generic.go b/internal/render/generic.go index cb5ae061..3a67550a 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -61,16 +61,15 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { if !ok { 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 { return err } - n, ok := row.Cells[0].(string) if !ok { 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 = append(r.Fields, nns) var ageCell interface{} @@ -95,26 +94,35 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { // ---------------------------------------------------------------------------- // Helpers... -func resourceNS(raw []byte) (string, error) { +func resourceNS(raw []byte) (string, string, error) { var obj map[string]interface{} + var ns, name string err := json.Unmarshal(raw, &obj) if err != nil { - return "", err + return ns, name, err } meta, ok := obj["metadata"].(map[string]interface{}) 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 { - return client.ClusterScope, nil + return client.ClusterScope, name, nil } - nns, ok := ns.(string) + ns, ok = ins.(string) 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 } diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index 2b441456..a588a852 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -21,7 +21,7 @@ func TestGenericRender(t *testing.T) { "withNS": { ns: "ns1", table: makeNSGeneric(), - eID: "ns1/c1", + eID: "ns1/fred", eFields: render.Fields{"ns1", "c1", "c2", "c3"}, eHeader: render.Header{ render.HeaderColumn{Name: "NAMESPACE"}, @@ -33,7 +33,7 @@ func TestGenericRender(t *testing.T) { "all": { ns: client.NamespaceAll, table: makeNSGeneric(), - eID: "ns1/c1", + eID: "ns1/fred", eFields: render.Fields{"ns1", "c1", "c2", "c3"}, eHeader: render.Header{ render.HeaderColumn{Name: "NAMESPACE"}, @@ -45,7 +45,7 @@ func TestGenericRender(t *testing.T) { "allNS": { ns: client.AllNamespaces, table: makeNSGeneric(), - eID: "ns1/c1", + eID: "ns1/fred", eFields: render.Fields{"ns1", "c1", "c2", "c3"}, eHeader: render.Header{ render.HeaderColumn{Name: "NAMESPACE"}, @@ -57,7 +57,7 @@ func TestGenericRender(t *testing.T) { "clusterWide": { ns: client.ClusterScope, table: makeNoNSGeneric(), - eID: "-/c1", + eID: "-/fred", eFields: render.Fields{"-", "c1", "c2", "c3"}, eHeader: render.Header{ render.HeaderColumn{Name: "NAMESPACE"}, @@ -69,7 +69,7 @@ func TestGenericRender(t *testing.T) { "age": { ns: client.ClusterScope, table: makeAgeGeneric(), - eID: "-/c1", + eID: "-/fred", eFields: render.Fields{"-", "c1", "c2", "Age"}, eHeader: render.Header{ render.HeaderColumn{Name: "NAMESPACE"}, diff --git a/internal/view/event.go b/internal/view/event.go index 0c531599..c39b3f5f 100644 --- a/internal/view/event.go +++ b/internal/view/event.go @@ -19,7 +19,7 @@ func NewEvent(gvr client.GVR) ResourceViewer { } e.GetTable().SetColorerFn(render.Event{}.ColorerFunc()) e.AddBindKeysFn(e.bindKeys) - e.GetTable().SetSortCol(ageCol, true) + e.GetTable().SetSortCol("LAST SEEN", false) return &e } @@ -27,9 +27,10 @@ func NewEvent(gvr client.GVR) ResourceViewer { func (e *Event) bindKeys(aa ui.KeyActions) { aa.Delete(tcell.KeyCtrlD, ui.KeyE) 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.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), }) } diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index 360fb161..85a10c80 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -3,6 +3,7 @@ package view import ( "fmt" "math" + "strconv" "strings" "github.com/derailed/k9s/internal/port" @@ -39,20 +40,20 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe p1, p2 := pf.ToPortSpec(ports) fieldLen := int(math.Max(30, float64(len(p1)))) f.AddInputField("Container Port:", p1, fieldLen, nil, nil) + f.AddInputField("Local Port:", p2, fieldLen, nil, nil) coField := f.GetFormItemByLabel("Container Port:").(*tview.InputField) + loField := f.GetFormItemByLabel("Local Port:").(*tview.InputField) if coField.GetText() == "" { 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) { port := extractPort(s) loField.SetText(port) p2 = port }) + if loField.GetText() == "" { + loField.SetPlaceholder("Enter a local port") + } f.AddInputField("Address:", address, fieldLen, nil, func(h string) { 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")) 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()) if err != nil { v.App().Flash().Err(err) @@ -122,10 +119,16 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) { // ---------------------------------------------------------------------------- // Helpers... -func extractPort(coPort string) string { - tokens := strings.Split(coPort, "::") +func extractPort(port string) string { + tokens := strings.Split(port, "::") 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] diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index c090e7b4..de089035 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -2,6 +2,7 @@ package view import ( "fmt" + "strings" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/port" @@ -41,16 +42,21 @@ func (p *PortForwardExtender) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - pod, err := p.fetchPodName(path) + p.fetchPodName(path) + pod, err := fetchPod(p.App().factory, path) if err != nil { p.App().Flash().Err(err) 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) return nil } - if err := showFwdDialog(p, pod, startFwdCB); err != nil { + if err := showFwdDialog(p, path, startFwdCB); err != nil { 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().QueueUpdateDraw(func() { - v.App().Flash().Infof("PortForward activated %s", pf.ID()) DismissPortForwards(v, v.App().Content.Pages) }) @@ -98,6 +103,7 @@ func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error { return err } + tt := make([]string, 0, len(pts)) 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) @@ -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) 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 }