adding cancelable launch prompts to NodeShell (#2360)
parent
dcec53e061
commit
f8ad4aa8c7
|
|
@ -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)
|
||||
}()
|
||||
}
|
||||
|
|
@ -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) {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue