release v0.25.8 (#1354)
parent
265e0561aa
commit
f97bceaf64
2
Makefile
2
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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
2
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
|
||||
|
|
|
|||
4
go.sum
4
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "<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
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue