adding cancelable launch prompts to NodeShell (#2360)

mine
Jayson Wang 2023-12-25 02:18:47 +08:00 committed by GitHub
parent dcec53e061
commit f8ad4aa8c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 157 additions and 23 deletions

View File

@ -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)
}()
}

View File

@ -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) {})
}
})
}

View File

@ -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"
@ -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 {
@ -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)
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
@ -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)

View File

@ -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"
)
@ -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
}