add pf indicator

mine
derailed 2020-06-29 18:47:02 -06:00
parent 766bf133da
commit f1111174aa
46 changed files with 353 additions and 99 deletions

View File

@ -0,0 +1,34 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.20.6
## 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 is as ever very much noticed and appreciated!
Also if you dig this tool, consider joining our [sponsorhip 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)
---
## First A Word From Our Sponsors...
First off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!
* [Remo Eichenberger](https://github.com/remoe)
* [Ken Ahrens](https://github.com/kenahrens)
Maintenance Release!
## Resolved Bugs/Features/PRs
* [Issue #778](https://github.com/derailed/k9s/issues/778)
* [Issue #774](https://github.com/derailed/k9s/issues/774)
* [Issue #761](https://github.com/derailed/k9s/issues/761)
* [Issue #759](https://github.com/derailed/k9s/issues/759)
* [Issue #756](https://github.com/derailed/k9s/issues/756)
---
<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)

View File

@ -24,14 +24,10 @@ import (
)
const (
cacheSize = 100
cacheExpiry = 5 * time.Minute
cacheMXKey = "metrics"
cacheMXAPIKey = "metricsAPI"
checkConnTimeout = 3 * time.Second
// CallTimeout represents default api call timeout.
CallTimeout = 5 * time.Second
cacheSize = 100
cacheExpiry = 5 * time.Minute
cacheMXKey = "metrics"
cacheMXAPIKey = "metricsAPI"
)
var supportedMetricsAPIVersions = []string{"v1beta1"}
@ -157,7 +153,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error)
}
client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr)
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
defer cancel()
for _, v := range verbs {
sar.Spec.ResourceAttributes.Verb = v
@ -203,7 +199,7 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
defer cancel()
nn, err := dial.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
@ -235,7 +231,6 @@ func (a *APIClient) CheckConnectivity() bool {
a.connOK = false
return a.connOK
}
cfg.Timeout = checkConnTimeout
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
log.Error().Err(err).Msgf("Unable to connect to api server")
@ -280,7 +275,13 @@ func (a *APIClient) HasMetrics() bool {
a.cache.Add(cacheMXKey, flag, cacheExpiry)
return flag
}
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
timeout, err := time.ParseDuration(*a.config.flags.Timeout)
if err != nil {
log.Error().Err(err).Msgf("parsing duration")
return false
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil {
flag = true

View File

@ -5,6 +5,7 @@ import (
"fmt"
"strings"
"sync"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
@ -14,8 +15,10 @@ import (
)
const (
defaultQPS = 50
defaultBurst = 50
defaultQPS = 50
defaultBurst = 50
defaultCallTimeoutDuration time.Duration = 5 * time.Second
defaultCallTimeout = "5s"
)
// Config tracks a kubernetes configuration.
@ -29,12 +32,34 @@ type Config struct {
// NewConfig returns a new k8s config or an error if the flags are invalid.
func NewConfig(f *genericclioptions.ConfigFlags) *Config {
timeout := defaultCallTimeout
if f.Timeout == nil {
f.Timeout = &timeout
} else {
_, err := time.ParseDuration(*f.Timeout)
if err != nil {
f.Timeout = &timeout
}
}
return &Config{
flags: f,
mutex: &sync.RWMutex{},
}
}
// CallTimeout returns the call timeout if set or the default if not set.
func (c *Config) CallTimeout() time.Duration {
if c.flags.Timeout == nil {
return defaultCallTimeoutDuration
}
dur, err := time.ParseDuration(*c.flags.Timeout)
if err != nil {
return defaultCallTimeoutDuration
}
return dur
}
// Flags returns configuration flags.
func (c *Config) Flags() *genericclioptions.ConfigFlags {
return c.flags

View File

@ -148,7 +148,7 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("users", "user", "usr")
a.declare("groups", "group", "grp")
a.declare("portforwards", "portforward", "pf")
a.declare("benchmarks", "benchmark", "be")
a.declare("benchmarks", "bench", "benchmark", "be")
a.declare("screendumps", "screendump", "sd")
a.declare("pulses", "pulse", "pu", "hz")
a.declare("xrays", "xray", "x")

View File

@ -215,9 +215,9 @@ func newStyle() Style {
func newCharts() Charts {
return Charts{
BgColor: "default",
DialBgColor: "default",
ChartBgColor: "default",
BgColor: "black",
DialBgColor: "black",
ChartBgColor: "black",
DefaultDialColors: Colors{Color("palegreen"), Color("orangered")},
DefaultChartColors: Colors{Color("palegreen"), Color("orangered")},
ResourceColors: map[string]Colors{

View File

@ -6,9 +6,11 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
)
@ -38,16 +40,23 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error
if !ok {
return nil, errors.New("no benchmark dir found in context")
}
path, _ := ctx.Value(internal.KeyPath).(string)
ff, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
oo := make([]runtime.Object, len(ff))
for i, f := range ff {
oo[i] = render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())}
oo := make([]runtime.Object, 0, len(ff))
for _, f := range ff {
log.Debug().Msgf("BENCH-LIST %q::%q", strings.Replace(path, "/", "_", 1), f.Name())
if path != "" && !strings.HasPrefix(f.Name(), strings.Replace(path, "/", "_", 1)) {
log.Debug().Msgf(" SKIP...")
continue
}
oo = append(oo, render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())})
}
log.Debug().Msgf("BENCH-FILES %#v", oo)
return oo, nil
}

View File

@ -39,26 +39,19 @@ func (c *CronJob) Run(path string) error {
return fmt.Errorf("user is not authorize to run cronjobs")
}
// BOZO!! Factory resource??
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
o, err := c.Factory.Get("batch/v1beta1/cronjobs", path, true, labels.Everything())
if err != nil {
return err
}
var cj batchv1beta1.CronJob
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)
if err != nil {
return errors.New("expecting CronJob resource")
}
var jobName = cj.Name
if len(cj.Name) >= maxJobNameSize {
jobName = cj.Name[0:maxJobNameSize]
}
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName + "-manual-" + rand.String(3),
@ -71,6 +64,8 @@ func (c *CronJob) Run(path string) error {
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), c.Client().Config().CallTimeout())
defer cancel()
_, err = dial.BatchV1().Jobs(ns).Create(ctx, job, metav1.CreateOptions{})
return err

View File

@ -118,14 +118,15 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
PropagationPolicy: &p,
GracePeriodSeconds: grace,
}
// BOZO!! Move to caller!
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel()
dial, err := g.dynClient()
if err != nil {
return err
}
// BOZO!! Move to caller!
ctx, cancel := context.WithTimeout(context.Background(), g.Client().Config().CallTimeout())
defer cancel()
if client.IsClusterScoped(ns) {
return dial.Delete(ctx, n, opts)
}

View File

@ -201,7 +201,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
for _, co := range po.Spec.InitContainers {
log.Debug().Msgf("Tailing INIT-CO %q", co.Name)
opts.Container = co.Name
if err := p.TailLogs(ctx, c, opts); err != nil {
if err := tailLogs(ctx, p, c, opts); err != nil {
return err
}
tailed = true

View File

@ -40,21 +40,25 @@ func (p *PortForward) Delete(path string, cascade, force bool) error {
return nil
}
// List returns a collection of screen dumps.
// List returns a collection of port forwards.
func (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, error) {
benchFile, ok := ctx.Value(internal.KeyBenchCfg).(string)
if !ok {
return nil, fmt.Errorf("no bench file found in context")
}
path, _ := ctx.Value(internal.KeyPath).(string)
config, err := config.NewBench(benchFile)
if err != nil {
log.Debug().Msgf("No custom benchmark config file found")
}
cc := config.Benchmarks.Containers
oo := make([]runtime.Object, 0, len(p.Factory.Forwarders()))
for k, f := range p.Factory.Forwarders() {
ff, cc := p.Factory.Forwarders(), config.Benchmarks.Containers
oo := make([]runtime.Object, 0, len(ff))
for k, f := range ff {
if !strings.HasPrefix(k, path) {
continue
}
cfg := render.BenchCfg{
C: config.Benchmarks.Defaults.C,
N: config.Benchmarks.Defaults.N,

View File

@ -103,8 +103,14 @@ func (p *PortForwarder) HasPortMapping(m string) bool {
}
// Start initiates a port forward session for a given pod and ports.
func (p *PortForwarder) Start(path, co string, t client.PortTunnel) (*portforward.PortForwarder, error) {
fwds := []string{t.PortMap()}
func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) {
if len(tt) == 0 {
return nil, fmt.Errorf("no ports assigned")
}
fwds := make([]string, 0, len(tt))
for _, t := range tt {
fwds = append(fwds, t.PortMap())
}
p.path, p.container, p.ports, p.age = path, co, fwds, time.Now()
ns, n := client.Namespaced(path)
@ -152,7 +158,7 @@ func (p *PortForwarder) Start(path, co string, t client.PortTunnel) (*portforwar
Name(n).
SubResource("portforward")
return p.forwardPorts("POST", req.URL(), t.Address, fwds)
return p.forwardPorts("POST", req.URL(), tt[0].Address, fwds)
}
func (p *PortForwarder) forwardPorts(method string, url *url.URL, address string, ports []string) (*portforward.PortForwarder, error) {

View File

@ -92,7 +92,7 @@ func (c *ClusterInfo) Refresh() {
data.K9sVer = c.version
data.K8sVer = c.cluster.Version()
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout())
defer cancel()
var mx client.ClusterMetrics
if err := c.cluster.Metrics(ctx, &mx); err == nil {

View File

@ -83,15 +83,16 @@ func (c *CmdBuff) Delete() {
}
// ClearText clears out command buffer.
func (c *CmdBuff) ClearText() {
func (c *CmdBuff) ClearText(fire bool) {
c.buff = make([]rune, 0, maxBuff)
c.fireBufferChanged()
if fire {
c.fireBufferChanged()
}
}
// Reset clears out the command buffer and deactivates it.
func (c *CmdBuff) Reset() {
c.ClearText()
c.fireBufferChanged()
c.ClearText(true)
c.SetActive(false)
}

View File

@ -62,7 +62,7 @@ func TestCmdBuffChanged(t *testing.T) {
assert.Equal(t, "", b.GetText())
b.Add('c')
b.ClearText()
b.ClearText(true)
assert.Equal(t, 0, l.act)
assert.Equal(t, 0, l.inact)
assert.Equal(t, "", l.text)

View File

@ -33,7 +33,7 @@ func TestTableReconcile(t *testing.T) {
err := ta.reconcile(ctx)
assert.Nil(t, err)
data := ta.Peek()
assert.Equal(t, 17, len(data.Header))
assert.Equal(t, 18, len(data.Header))
assert.Equal(t, 1, len(data.RowEvents))
assert.Equal(t, client.NamespaceAll, data.Namespace)
}
@ -106,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
assert.Equal(t, 1, len(rr))
assert.Equal(t, 17, len(rr[0].Fields))
assert.Equal(t, 18, len(rr[0].Fields))
}
func TestTableGenericHydrate(t *testing.T) {

View File

@ -33,7 +33,7 @@ func TestTableRefresh(t *testing.T) {
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
ta.Refresh(ctx)
data := ta.Peek()
assert.Equal(t, 17, len(data.Header))
assert.Equal(t, 18, len(data.Header))
assert.Equal(t, 1, len(data.RowEvents))
assert.Equal(t, client.NamespaceAll, data.Namespace)
assert.Equal(t, 1, l.count)

View File

@ -47,7 +47,6 @@ func NewBenchmark(base, version string, cfg config.BenchConfig) (*Benchmark, err
}
func (b *Benchmark) init(base, version string) error {
log.Debug().Msgf("BENCH-INIT")
req, err := http.NewRequest(b.config.HTTP.Method, base, nil)
if err != nil {
return err
@ -111,7 +110,6 @@ func (b *Benchmark) Canceled() bool {
// Run starts a benchmark,
func (b *Benchmark) Run(cluster string, done func()) {
log.Debug().Msgf("Running benchmark on cluster %s", cluster)
log.Debug().Msgf("BENCH-CFG %#v", b.worker)
buff := new(bytes.Buffer)
b.worker.Writer = buff
// this call will block until the benchmark is complete or timesout.

View File

@ -16,7 +16,7 @@ type Alias struct{}
// ColorerFunc colors a resource row.
func (Alias) ColorerFunc() ColorerFunc {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorMediumSpringGreen
return tcell.ColorAliceBlue
}
}

View File

@ -25,15 +25,15 @@ func TestAliasColorer(t *testing.T) {
"addAll": {
ns: client.AllNamespaces,
re: render.RowEvent{Kind: render.EventAdd, Row: r},
e: tcell.ColorMediumSpringGreen},
e: tcell.ColorAliceBlue},
"deleteAll": {
ns: client.AllNamespaces,
re: render.RowEvent{Kind: render.EventDelete, Row: r},
e: tcell.ColorMediumSpringGreen},
e: tcell.ColorAliceBlue},
"updateAll": {
ns: client.AllNamespaces,
re: render.RowEvent{Kind: render.EventUpdate, Row: r},
e: tcell.ColorMediumSpringGreen,
e: tcell.ColorAliceBlue,
},
}

View File

@ -57,7 +57,7 @@ func (Benchmark) Header(ns string) Header {
func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
bench, ok := o.(BenchInfo)
if !ok {
return fmt.Errorf("expecting benchinfo but got `%T", o)
return fmt.Errorf("No benchmarks available %T", o)
}
data, err := b.readFile(bench.Path)

View File

@ -69,6 +69,7 @@ func (c Container) ColorerFunc() ColorerFunc {
func (Container) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "PF"},
HeaderColumn{Name: "IMAGE"},
HeaderColumn{Name: "READY"},
HeaderColumn{Name: "STATE"},
@ -103,6 +104,7 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
r.ID = co.Container.Name
r.Fields = Fields{
co.Container.Name,
"●",
co.Container.Image,
ready,
state,

View File

@ -28,6 +28,7 @@ func TestContainer(t *testing.T) {
assert.Equal(t, "fred", r.ID)
assert.Equal(t, render.Fields{
"fred",
"●",
"img",
"false",
"Running",

View File

@ -19,7 +19,7 @@ type Generic struct {
ageIndex int
}
// Happy returns true if resoure is happy, false otherwise
// Happy returns true if resource is happy, false otherwise
func (Generic) Happy(ns string, r Row) bool {
return true
}
@ -79,6 +79,10 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
ageCell = c
continue
}
if c == nil {
r.Fields = append(r.Fields, Blank)
continue
}
r.Fields = append(r.Fields, fmt.Sprintf("%v", c))
}
if ageCell != nil {

View File

@ -59,6 +59,7 @@ func (Pod) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "PF"},
HeaderColumn{Name: "READY"},
HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
HeaderColumn{Name: "STATUS"},
@ -97,6 +98,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
r.Fields = Fields{
po.Namespace,
po.ObjectMeta.Name,
"●",
strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss)),
strconv.Itoa(rc),
phase,

View File

@ -159,8 +159,8 @@ func TestPodRender(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID)
e := render.Fields{"default", "nginx", "1/1", "0", "Running", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"}
assert.Equal(t, e, r.Fields[:14])
e := render.Fields{"default", "nginx", "●", "1/1", "0", "Running", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"}
assert.Equal(t, e, r.Fields[:15])
}
func BenchmarkPodRender(b *testing.B) {
@ -190,8 +190,8 @@ func TestPodInitRender(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID)
e := render.Fields{"default", "nginx", "1/1", "0", "Init:0/1", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"}
assert.Equal(t, e, r.Fields[:14])
e := render.Fields{"default", "nginx", "●", "1/1", "0", "Init:0/1", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"}
assert.Equal(t, e, r.Fields[:15])
}
// ----------------------------------------------------------------------------

View File

@ -14,6 +14,11 @@ func NewTableData() *TableData {
return &TableData{}
}
// IndexOfHeader return the index of the header.
func (t *TableData) IndexOfHeader(h string) int {
return t.Header.IndexOf(h, false)
}
// Labelize prints out specific label columns
func (t *TableData) Labelize(labels []string) TableData {
labelCol := t.Header.IndexOf("LABELS", true)

View File

@ -26,6 +26,9 @@ const (
// Pending represents a pod pending status.
Pending = "Pending"
// Blank represents no value.
Blank = ""
)
const (

View File

@ -188,7 +188,7 @@ func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
if !a.CmdBuff().IsActive() {
return evt
}
a.CmdBuff().ClearText()
a.CmdBuff().ClearText(true)
return nil
}
@ -198,7 +198,7 @@ func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
a.ResetPrompt(a.cmdModel)
a.cmdModel.ClearText()
a.cmdModel.ClearText(true)
return nil
}

View File

@ -43,7 +43,7 @@ type PromptModel interface {
GetText() string
// ClearText clears out model text.
ClearText()
ClearText(fire bool)
// Notify notifies all listener of current suggestions.
Notify()
@ -135,13 +135,13 @@ func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey {
case tcell.KeyRune:
p.model.Add(evt.Rune())
case tcell.KeyEscape:
p.model.ClearText()
p.model.ClearText(true)
p.model.SetActive(false)
case tcell.KeyEnter, tcell.KeyCtrlE:
p.model.SetText(p.model.GetText())
p.model.SetActive(false)
case tcell.KeyCtrlW, tcell.KeyCtrlU:
p.model.ClearText()
p.model.ClearText(true)
case tcell.KeyUp:
if s, ok := m.NextSuggestion(); ok {
p.suggest(p.model.GetText(), s)

View File

@ -382,7 +382,7 @@ func (t *Table) filtered(data render.TableData) render.TableData {
filtered, err := rxFilter(t.cmdBuff.GetText(), filtered)
if err != nil {
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp")
t.cmdBuff.ClearText()
t.cmdBuff.ClearText(true)
}
return filtered

View File

@ -24,8 +24,8 @@ func NewAlias(gvr client.GVR) ResourceViewer {
ResourceViewer: NewBrowser(gvr),
}
a.GetTable().SetColorerFn(render.Alias{}.ColorerFunc())
a.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)
a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone)
a.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue)
a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone)
a.SetBindKeysFn(a.bindKeys)
a.SetContextFn(a.aliasContext)

View File

@ -29,7 +29,7 @@ var ExitStatus = ""
const (
splashDelay = 1 * time.Second
clusterRefresh = 5 * time.Second
clusterRefresh = 15 * time.Second
maxConRetry = 15
clusterInfoWidth = 50
clusterInfoPad = 15
@ -284,6 +284,7 @@ func (a *App) refreshCluster() {
} else {
a.ClearStatus(true)
}
a.factory.ValidatePortForwards()
} else {
atomic.AddInt32(&a.conRetry, 1)
if c != nil {
@ -337,7 +338,7 @@ func (a *App) isValidNS(ns string) (bool, error) {
return true, nil
}
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), a.Conn().Config().CallTimeout())
defer cancel()
dial, err := a.Conn().Dial()
if err != nil {

View File

@ -130,7 +130,9 @@ func (b *Browser) Start() {
// Stop terminates browser updates.
func (b *Browser) Stop() {
log.Debug().Msgf("BRO-STOP %v", b.GVR())
if b.cancelFn != nil {
log.Debug().Msgf("Canceling!!")
b.cancelFn()
b.cancelFn = nil
}
@ -153,7 +155,7 @@ func (b *Browser) BufferActive(state bool, k model.BufferKind) {
if state {
return
}
b.GetModel().Refresh(b.defaultContext())
b.GetModel().Refresh(b.prepareContext())
b.app.QueueUpdateDraw(func() {
b.Update(b.GetModel().Peek())
if b.GetRowCount() > 1 {
@ -242,7 +244,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !b.CmdBuff().InCmdMode() {
b.CmdBuff().Reset()
b.CmdBuff().ClearText(false)
return b.App().PrevCmd(evt)
}

View File

@ -1,10 +1,12 @@
package view
import (
"context"
"errors"
"fmt"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
@ -28,10 +30,24 @@ func NewContainer(gvr client.GVR) ResourceViewer {
c.GetTable().SetEnterFn(c.viewLogs)
c.GetTable().SetColorerFn(render.Container{}.ColorerFunc())
c.SetBindKeysFn(c.bindKeys)
c.GetTable().SetDecorateFn(c.portForwardIndicator)
return &c
}
func (c *Container) portForwardIndicator(data render.TableData) render.TableData {
ff := c.App().factory.Forwarders()
col := data.IndexOfHeader("PF")
for _, re := range data.RowEvents {
if ff.IsContainerForwarded(c.GetTable().Path, re.Row.ID) {
re.Row.Fields[col] = "[orange::b]Ⓕ"
}
}
return data
}
// Name returns the component name.
func (c *Container) Name() string { return containerTitle }
@ -50,6 +66,7 @@ func (c *Container) bindKeys(aa ui.KeyActions) {
}
aa.Add(ui.KeyActions{
ui.KeyF: ui.NewKeyAction("Show PortForward", c.showPFCmd, true),
ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false),
})
@ -60,7 +77,7 @@ func (c *Container) k9sEnv() Env {
path := c.GetTable().GetSelectedItem()
row, ok := c.GetTable().GetSelectedRow(path)
if !ok {
log.Error().Msgf("unable to locate seleted row for %q", path)
log.Error().Msgf("unable to locate selected row for %q", path)
}
env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header, row)
env["NAMESPACE"], env["POD"] = client.Namespaced(c.GetTable().Path)
@ -79,6 +96,30 @@ func (c *Container) viewLogs(app *App, model ui.Tabular, gvr, path string) {
// Handlers...
func (c *Container) showPFCmd(evt *tcell.EventKey) *tcell.EventKey {
path := c.GetTable().GetSelectedItem()
if path == "" {
return evt
}
if !c.App().factory.Forwarders().IsContainerForwarded(c.GetTable().Path, path) {
c.App().Flash().Errf("no portforwards defined")
return nil
}
pf := NewPortForward(client.NewGVR("portforwards"))
pf.SetContextFn(c.portForwardContext)
if err := c.App().inject(pf); err != nil {
c.App().Flash().Err(err)
}
return nil
}
func (c *Container) portForwardContext(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, internal.KeyBenchCfg, c.App().BenchFile)
return context.WithValue(ctx, internal.KeyPath, c.GetTable().Path)
}
func (c *Container) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := c.GetTable().GetSelectedItem()
if sel == "" {
@ -151,7 +192,7 @@ func (c *Container) isForwardable(path string) ([]string, bool) {
}
}
if cs == nil {
log.Error().Err(fmt.Errorf("unable to locate container status named %q", path))
log.Error().Err(fmt.Errorf("unable to locate container status for %q", path))
return nil, false
}

View File

@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) {
assert.Nil(t, c.Init(makeCtx()))
assert.Equal(t, "Containers", c.Name())
assert.Equal(t, 17, len(c.Hints()))
assert.Equal(t, 18, len(c.Hints()))
}

View File

@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
v := view.NewHelp()
assert.Nil(t, v.Init(ctx))
assert.Equal(t, 23, v.GetRowCount())
assert.Equal(t, 24, v.GetRowCount())
assert.Equal(t, 8, v.GetColumnCount())
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))

View File

@ -155,7 +155,7 @@ func (n *Node) yamlCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), n.App().Conn().Config().CallTimeout())
defer cancel()
sel := n.GetTable().GetSelectedItem()

View File

@ -56,13 +56,25 @@ func (p *PortForward) bindKeys(aa ui.KeyActions) {
}
func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
if err := p.App().inject(NewBenchmark(client.NewGVR("benchmarks"))); err != nil {
b := NewBenchmark(client.NewGVR("benchmarks"))
b.SetContextFn(p.getContext)
if err := p.App().inject(b); err != nil {
p.App().Flash().Err(err)
}
return nil
}
func (p *PortForward) getContext(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, internal.KeyDir, benchDir(p.App().Config))
path := p.GetTable().GetSelectedItem()
if path == "" {
return ctx
}
return context.WithValue(ctx, internal.KeyPath, path)
}
func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
if p.bench != nil {
p.App().Status(model.FlashErr, "Benchmark Canceled!")

View File

@ -13,7 +13,7 @@ import (
const portForwardKey = "portforward"
// PortForwardCB represents a port-forward callback function.
type PortForwardCB func(v ResourceViewer, path, co string, mapper client.PortTunnel)
type PortForwardCB func(v ResourceViewer, path, co string, mapper []client.PortTunnel)
// ShowPortForwards pops a port forwarding configuration dialog.
func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardCB) {
@ -42,12 +42,29 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortFo
pages := v.App().Content.Pages
f.AddButton("OK", func() {
tunnel := client.PortTunnel{
Address: address,
LocalPort: p2,
ContainerPort: extractPort(p1),
pp1 := strings.Split(p1, ",")
pp2 := strings.Split(p2, ",")
if len(pp1) == 0 || len(pp1) != len(pp2) {
v.App().Flash().Err(fmt.Errorf("container to local port mismatch"))
return
}
okFn(v, path, extractContainer(p1), tunnel)
for _, p := range pp1 {
if !hasPort(p, ports) {
v.App().Flash().Err(fmt.Errorf("container port must match exposed ports"))
return
}
}
var tt []client.PortTunnel
for i := range pp1 {
tt = append(tt, client.PortTunnel{
Address: address,
LocalPort: pp2[i],
ContainerPort: extractPort(pp1[i]),
})
}
okFn(v, path, extractContainer(pp1[0]), tt)
})
f.AddButton("Cancel", func() {
DismissPortForwards(v, pages)
@ -73,6 +90,16 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) {
// ----------------------------------------------------------------------------
// Helpers...
func hasPort(port string, pp []string) bool {
for _, p := range pp {
if p != port {
return false
}
}
return true
}
func extractPort(p string) string {
rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(UDP)?\z`)
mm := rx.FindStringSubmatch(p)

View File

@ -49,6 +49,10 @@ func (p *PortForwardExtender) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
p.App().Flash().Err(err)
return nil
}
if p.App().factory.Forwarders().IsPodForwarded(pod) {
p.App().Flash().Errf("A PortForward already exist for pod %s", pod)
return nil
}
if err := showFwdDialog(p, pod, startFwdCB); err != nil {
p.App().Flash().Err(err)
}
@ -100,11 +104,13 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward
})
}
func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) {
err := tryListenPort(t.Address, t.LocalPort)
if err != nil {
v.App().Flash().Err(err)
return
func startFwdCB(v ResourceViewer, path, co string, tt []client.PortTunnel) {
for _, t := range tt {
err := tryListenPort(t.Address, t.LocalPort)
if err != nil {
v.App().Flash().Err(err)
return
}
}
if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, co)); ok {
@ -113,13 +119,13 @@ func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) {
}
pf := dao.NewPortForwarder(v.App().factory)
fwd, err := pf.Start(path, co, t)
fwd, err := pf.Start(path, co, tt)
if err != nil {
v.App().Flash().Err(err)
return
}
log.Debug().Msgf(">>> Starting port forward %q %#v", path, t)
log.Debug().Msgf(">>> Starting port forward %q %#v", path, tt)
go runForward(v, pf, fwd)
}

View File

@ -34,10 +34,24 @@ func NewPod(gvr client.GVR) ResourceViewer {
p.SetBindKeysFn(p.bindKeys)
p.GetTable().SetEnterFn(p.showContainers)
p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc())
p.GetTable().SetDecorateFn(p.portForwardIndicator)
return &p
}
func (p *Pod) portForwardIndicator(data render.TableData) render.TableData {
ff := p.App().factory.Forwarders()
col := data.IndexOfHeader("PF")
for _, re := range data.RowEvents {
if ff.IsPodForwarded(re.Row.ID) {
re.Row.Fields[col] = "[orange::b]Ⓕ"
}
}
return data
}
func (p *Pod) bindDangerousKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
tcell.KeyCtrlK: ui.NewKeyAction("Kill", p.killCmd, true),
@ -52,6 +66,7 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
}
aa.Add(ui.KeyActions{
ui.KeyF: ui.NewKeyAction("Show PortForward", p.showPFCmd, true),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(readyCol, true), false),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd("RESTARTS", false), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(statusCol, true), false),
@ -90,7 +105,31 @@ func (p *Pod) coContext(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem())
}
// Commands...
// Handlers...
func (p *Pod) showPFCmd(evt *tcell.EventKey) *tcell.EventKey {
path := p.GetTable().GetSelectedItem()
if path == "" {
return evt
}
if !p.App().factory.Forwarders().IsPodForwarded(path) {
p.App().Flash().Errf("no portforwards defined")
return nil
}
pf := NewPortForward(client.NewGVR("portforwards"))
pf.SetContextFn(p.portForwardContext)
if err := p.App().inject(pf); err != nil {
p.App().Flash().Err(err)
}
return nil
}
func (p *Pod) portForwardContext(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile)
return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem())
}
func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey {
sels := p.GetTable().GetSelectedItems()

View File

@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "Pods", po.Name())
assert.Equal(t, 22, len(po.Hints()))
assert.Equal(t, 23, len(po.Hints()))
}
// Helpers...

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
@ -45,7 +44,7 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
msg = fmt.Sprintf("Restart %d deployments?", len(paths))
}
dialog.ShowConfirm(r.App().Content.Pages, "Confirm Restart", msg, func() {
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), r.App().Conn().Config().CallTimeout())
defer cancel()
for _, path := range paths {
if err := r.restartRollout(ctx, path); err != nil {

View File

@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
@ -75,7 +74,7 @@ func (s *ScaleExtender) makeScaleForm(sel string) *tview.Form {
s.App().Flash().Err(err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
defer cancel()
if err := s.scale(ctx, sel, count); err != nil {
log.Error().Err(err).Msgf("DP %s scaling failed", sel)

View File

@ -2,6 +2,7 @@ package watch
import (
"fmt"
"strings"
"sync"
"time"
@ -243,8 +244,11 @@ func (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, err
func (f *Factory) AddForwarder(pf Forwarder) {
f.mx.Lock()
defer f.mx.Unlock()
f.forwarders[pf.Path()] = pf
for k, v := range f.forwarders {
log.Debug().Msgf("%q -- %#v", k, v)
}
}
// DeleteForwarder deletes portforward for a given container.
@ -266,6 +270,21 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) {
f.mx.RLock()
defer f.mx.RUnlock()
for k := range f.forwarders {
log.Debug().Msgf("KEY %q::%q", k, path)
}
fwd, ok := f.forwarders[path]
return fwd, ok
}
// Validate check if pods are still around for portforwards.
func (f *Factory) ValidatePortForwards() {
for k, fwd := range f.forwarders {
tokens := strings.Split(k, ":")
_, err := f.Get("v1/pods", tokens[0], false, labels.Everything())
if err != nil {
fwd.Stop()
delete(f.forwarders, k)
}
}
}

View File

@ -11,7 +11,7 @@ import (
// Forwarder represents a port forwarder.
type Forwarder interface {
// Start starts a port-forward.
Start(path, co string, t client.PortTunnel) (*portforward.PortForwarder, error)
Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error)
// Stop terminates a port forward.
Stop()
@ -49,6 +49,24 @@ func NewForwarders() Forwarders {
return make(map[string]Forwarder)
}
// IsForwarded checks if pod has a forward
func (ff Forwarders) IsPodForwarded(path string) bool {
for k := range ff {
fqn := strings.Split(k, ":")
if fqn[0] == path {
return true
}
}
return false
}
// IsForwarded checks if pod has a forward
func (ff Forwarders) IsContainerForwarded(path, co string) bool {
_, ok := ff[path+":"+co]
return ok
}
// DeleteAll stops and delete all port-forwards.
func (ff Forwarders) DeleteAll() {
for k, f := range ff {