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/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"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/fatih/color"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -233,25 +235,50 @@ func clearScreen() {
|
||||||
|
|
||||||
const (
|
const (
|
||||||
k9sShell = "k9s-shell"
|
k9sShell = "k9s-shell"
|
||||||
k9sShellRetryCount = 10
|
k9sShellRetryCount = 50
|
||||||
k9sShellRetryDelay = 10 * time.Second
|
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 {
|
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() {
|
defer func() {
|
||||||
if err := nukeK9sShell(a); err != nil {
|
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 {
|
func sshIn(a *App, fqn, co string) error {
|
||||||
|
|
@ -309,31 +336,33 @@ func nukeK9sShell(a *App) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func launchShellPod(a *App, node string) error {
|
func launchShellPod(ctx context.Context, a *App, node string) error {
|
||||||
a.Flash().Infof("Launching node shell on %s...", node)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
spo = a.Config.K9s.ShellPod
|
spo = a.Config.K9s.ShellPod
|
||||||
spec = k9sShellPod(node, spo)
|
spec = k9sShellPod(node, spo)
|
||||||
)
|
)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
dial, err := a.Conn().Dial()
|
dial, err := a.Conn().Dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn := dial.CoreV1().Pods(spo.Namespace)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < k9sShellRetryCount; i++ {
|
for i := 0; i < k9sShellRetryCount; i++ {
|
||||||
o, err := a.factory.Get("v1/pods", client.FQN(spo.Namespace, k9sShellPodName()), true, labels.Everything())
|
o, err := a.factory.Get("v1/pods", client.FQN(spo.Namespace, k9sShellPodName()), true, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
time.Sleep(k9sShellRetryDelay)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(k9sShellRetryDelay):
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var pod v1.Pod
|
var pod v1.Pod
|
||||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil {
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -342,7 +371,12 @@ func launchShellPod(a *App, node string) error {
|
||||||
if pod.Status.Phase == v1.PodRunning {
|
if pod.Status.Phase == v1.PodRunning {
|
||||||
return nil
|
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)
|
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"
|
||||||
"github.com/derailed/k9s/internal/ui/dialog"
|
"github.com/derailed/k9s/internal/ui/dialog"
|
||||||
"github.com/derailed/tcell/v2"
|
"github.com/derailed/tcell/v2"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -161,9 +160,7 @@ func (n *Node) sshCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
n.Stop()
|
n.Stop()
|
||||||
defer n.Start()
|
defer n.Start()
|
||||||
_, node := client.Namespaced(path)
|
_, node := client.Namespaced(path)
|
||||||
if err := ssh(n.App(), node); err != nil {
|
launchNodeShell(n, n.App(), node)
|
||||||
log.Error().Err(err).Msgf("Node Shell Failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue