diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index 8a84d7f7..bc48aa4b 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -107,6 +107,32 @@ func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, erro return refs, nil } +// SetSuspend a CronJob. +func (c *CronJob) SetSuspend(ctx context.Context, path string, suspend bool) error { + ns, n := client.Namespaced(path) + auth, err := c.Client().CanI(ns, "batch/v1beta1/CronJob", []string{client.GetVerb, client.UpdateVerb}) + if err != nil { + return err + } + if !auth { + return fmt.Errorf("user is not authorized to update a CronJob") + } + + dial, err := c.Client().Dial() + if err != nil { + return err + } + cronjob, err := dial.BatchV1beta1().CronJobs(ns).Get(ctx, n, metav1.GetOptions{}) + if err != nil { + return err + } + + cronjob.Spec.Suspend = &suspend + _, err = dial.BatchV1beta1().CronJobs(ns).Update(ctx, cronjob, metav1.UpdateOptions{}) + + return err +} + // Scan scans for cluster resource refs. func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go index 94bdf668..0db1a679 100644 --- a/internal/view/cronjob.go +++ b/internal/view/cronjob.go @@ -3,12 +3,14 @@ package view import ( "context" "fmt" + "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" "github.com/gdamore/tcell/v2" "github.com/rs/zerolog/log" batchv1beta1 "k8s.io/api/batch/v1beta1" @@ -17,6 +19,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +const suspendDialogKey = "suspend" + // CronJob represents a cronjob viewer. type CronJob struct { ResourceViewer @@ -64,9 +68,99 @@ func jobCtx(path, uid string) ContextFunc { func (c *CronJob) bindKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ tcell.KeyCtrlT: ui.NewKeyAction("Trigger", c.trigger, true), + ui.KeyShiftS: ui.NewKeyAction("ToggleSuspend", c.toggleSuspend, true), }) } +func (c *CronJob) toggleSuspend(evt *tcell.EventKey) *tcell.EventKey { + sel := c.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + c.Stop() + defer c.Start() + c.showSuspendDialog(sel) + + return nil +} + +func (c *CronJob) showSuspendDialog(sel string) { + cell := c.GetTable().GetCell(c.GetTable().GetSelectedRowIndex(), c.GetTable().NameColIndex() + 2) + if cell == nil { + return + } + suspended := strings.TrimSpace(cell.Text) == "true" + title := "Suspend" + if suspended { + title = "Unsuspend" + } + + confirm := tview.NewModalForm(fmt.Sprintf("<%s>", title), c.makeSuspendForm(sel, !suspended)) + confirm.SetText(fmt.Sprintf("%s CronJob %s", title, sel)) + confirm.SetDoneFunc(func(int, string) { + c.dismissDialog() + }) + c.App().Content.AddPage(suspendDialogKey, confirm, false, false) + c.App().Content.ShowPage(suspendDialogKey) +} + +func (c *CronJob) makeSuspendForm(sel string, suspend bool) *tview.Form { + f := c.makeStyledForm() + action := "suspended" + if !suspend { + action ="unsuspended" + } + + f.AddButton("OK", func() { + defer c.dismissDialog() + + ctx, cancel := context.WithTimeout(context.Background(), c.App().Conn().Config().CallTimeout()) + defer cancel() + if err := c.setSuspend(ctx, sel, suspend); err != nil { + log.Error().Err(err).Msgf("CronJOb %s %s failed", sel, action) + c.App().Flash().Err(err) + } else { + c.App().Flash().Infof("CronJOb %s %s successfully", sel, action) + } + }) + + f.AddButton("Cancel", func() { + c.dismissDialog() + }) + + return f +} + +func (c *CronJob) setSuspend(ctx context.Context, path string, suspend bool) error { + res, err := dao.AccessorFor(c.App().factory, c.GVR()) + if err != nil { + return nil + } + cronJob, ok := res.(*dao.CronJob) + if !ok { + return fmt.Errorf("expecting a scalable resource for %q", c.GVR()) + } + + return cronJob.SetSuspend(ctx, path, suspend) +} + +func (c *CronJob) makeStyledForm() *tview.Form { + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor). + SetButtonTextColor(tview.Styles.PrimaryTextColor). + SetLabelColor(tcell.ColorAqua). + SetFieldTextColor(tcell.ColorOrange) + + return f +} + +func (c *CronJob) dismissDialog() { + c.App().Content.RemovePage(suspendDialogKey) +} + func (c *CronJob) trigger(evt *tcell.EventKey) *tcell.EventKey { sel := c.GetTable().GetSelectedItem() if sel == "" {