Skip to content

Commit

Permalink
adding cancelable launch prompts to NodeShell (derailed#2360)
Browse files Browse the repository at this point in the history
  • Loading branch information
wjiec authored and thejoeejoee committed Jan 31, 2024
1 parent cef510f commit d340b72
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 23 deletions.
57 changes: 57 additions & 0 deletions internal/ui/dialog/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dialog

import (
"context"

"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
)

type promptAction func(ctx context.Context)

// ShowPrompt pops a prompt dialog.
func ShowPrompt(styles config.Dialog, pages *ui.Pages, title, msg string, action promptAction, cancel cancelFunc) {
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
SetButtonBackgroundColor(styles.ButtonBgColor.Color()).
SetButtonTextColor(styles.ButtonFgColor.Color()).
SetLabelColor(styles.LabelFgColor.Color()).
SetFieldTextColor(styles.FieldFgColor.Color())

ctx, cancelCtx := context.WithCancel(context.Background())

f.AddButton("Cancel", func() {
dismiss(pages)
cancelCtx()
cancel()
})

for i := 0; i < f.GetButtonCount(); i++ {
b := f.GetButton(i)
if b == nil {
continue
}
b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())
b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())
}

f.SetFocus(0)
modal := tview.NewModalForm("<"+title+">", f)
modal.SetText(msg)
modal.SetTextColor(styles.FgColor.Color())
modal.SetDoneFunc(func(int, string) {
dismiss(pages)
cancelCtx()
cancel()
})

pages.AddPage(dialogKey, modal, false, false)
pages.ShowPage(dialogKey)

go func() {
action(ctx)
dismiss(pages)
}()
}
46 changes: 46 additions & 0 deletions internal/ui/dialog/prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dialog

import (
"context"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
"testing"
"time"
)

func TestShowPrompt(t *testing.T) {
t.Run("waiting done", func(t *testing.T) {
a := tview.NewApplication()
p := ui.NewPages()
a.SetRoot(p, false)

ShowPrompt(config.Dialog{}, p, "Running", "Pod", func(context.Context) {
time.Sleep(time.Millisecond)
}, func() {
t.Errorf("unexpected cancellations")
})
})

t.Run("canceled", func(t *testing.T) {
a := tview.NewApplication()
p := ui.NewPages()
a.SetRoot(p, false)

go ShowPrompt(config.Dialog{}, p, "Running", "Pod", func(ctx context.Context) {
select {
case <-time.After(time.Second):
t.Errorf("expected cancellations")
case <-ctx.Done():
}
}, func() {})

time.Sleep(time.Second / 2)
d := p.GetPrimitive(dialogKey).(*tview.ModalForm)
if assert.NotNil(t, d) {
d.InputHandler()(tcell.NewEventKey(tcell.KeyEnter, '\n', 0), func(tview.Primitive) {})
}
})
}
72 changes: 53 additions & 19 deletions internal/view/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (

"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/fatih/color"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -233,25 +235,50 @@ func clearScreen() {

const (
k9sShell = "k9s-shell"
k9sShellRetryCount = 10
k9sShellRetryDelay = 10 * time.Second
k9sShellRetryCount = 50
k9sShellRetryDelay = 2 * time.Second
)

func ssh(a *App, node string) error {
func launchNodeShell(v model.Igniter, a *App, node string) {
if err := nukeK9sShell(a); err != nil {
return err
a.Flash().Errf("Cleaning node shell failed: %s", err)
return
}

msg := fmt.Sprintf("Launching node shell on %s...", node)
dialog.ShowPrompt(a.Styles.Dialog(), a.Content.Pages, "Launching", msg, func(ctx context.Context) {
err := launchShellPod(ctx, a, node)
if err != nil {
if !errors.Is(err, context.Canceled) {
a.Flash().Errf("Launching node shell failed: %s", err)
}
return
}

go launchPodShell(v, a)
}, func() {
if err := nukeK9sShell(a); err != nil {
a.Flash().Errf("Cleaning node shell failed: %s", err)
return
}
})
}

func launchPodShell(v model.Igniter, a *App) {
defer func() {
if err := nukeK9sShell(a); err != nil {
log.Error().Err(err).Msgf("nuking k9s shell pod")
a.Flash().Errf("Launching node shell failed: %s", err)
return
}
}()
if err := launchShellPod(a, node); err != nil {
return err
}
ns := a.Config.K9s.ShellPod.Namespace

return sshIn(a, client.FQN(ns, k9sShellPodName()), k9sShell)
v.Stop()
defer v.Start()

ns := a.Config.K9s.ShellPod.Namespace
if err := sshIn(a, client.FQN(ns, k9sShellPodName()), k9sShell); err != nil {
a.Flash().Errf("Launching node shell failed: %s", err)
}
}

func sshIn(a *App, fqn, co string) error {
Expand Down Expand Up @@ -309,31 +336,33 @@ func nukeK9sShell(a *App) error {
return err
}

func launchShellPod(a *App, node string) error {
a.Flash().Infof("Launching node shell on %s...", node)

func launchShellPod(ctx context.Context, a *App, node string) error {
var (
spo = a.Config.K9s.ShellPod
spec = k9sShellPod(node, spo)
)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

dial, err := a.Conn().Dial()
if err != nil {
return err
}

conn := dial.CoreV1().Pods(spo.Namespace)
if _, err := conn.Create(ctx, spec, metav1.CreateOptions{}); err != nil {
if _, err = conn.Create(ctx, spec, metav1.CreateOptions{}); err != nil {
return err
}

for i := 0; i < k9sShellRetryCount; i++ {
o, err := a.factory.Get("v1/pods", client.FQN(spo.Namespace, k9sShellPodName()), true, labels.Everything())
if err != nil {
time.Sleep(k9sShellRetryDelay)
continue
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(k9sShellRetryDelay):
continue
}
}

var pod v1.Pod
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil {
return err
Expand All @@ -342,7 +371,12 @@ func launchShellPod(a *App, node string) error {
if pod.Status.Phase == v1.PodRunning {
return nil
}
time.Sleep(k9sShellRetryDelay)

select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(k9sShellRetryDelay):
}
}

return fmt.Errorf("unable to launch shell pod on node %s", node)
Expand Down
5 changes: 1 addition & 4 deletions internal/view/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/derailed/tcell/v2"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -161,9 +160,7 @@ func (n *Node) sshCmd(evt *tcell.EventKey) *tcell.EventKey {
n.Stop()
defer n.Start()
_, node := client.Namespaced(path)
if err := ssh(n.App(), node); err != nil {
log.Error().Err(err).Msgf("Node Shell Failed")
}
launchNodeShell(n, n.App(), node)

return nil
}
Expand Down

0 comments on commit d340b72

Please sign in to comment.