From a333db0bb905660c1907852ed4af99afcd42cbe4 Mon Sep 17 00:00:00 2001 From: Shinto C V Date: Sun, 11 Feb 2024 22:55:39 +0530 Subject: [PATCH] [feat] Add stop job action (#131) Issue #120 --- internal/tui/components/app/app.go | 91 +++++++++++++++++++++++++++--- internal/tui/nomad/jobadmin.go | 61 ++++++++++++++++++++ internal/tui/nomad/pages.go | 34 ++++++++++- internal/tui/nomad/util.go | 7 ++- 4 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 internal/tui/nomad/jobadmin.go diff --git a/internal/tui/components/app/app.go b/internal/tui/components/app/app.go index 9af3b0a0..7a790955 100644 --- a/internal/tui/components/app/app.go +++ b/internal/tui/components/app/app.go @@ -211,7 +211,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case nomad.ExecPage: m.getCurrentPageModel().SetInputPrefix("Enter command: ") - case nomad.AllocAdminConfirmPage: + case nomad.AllocAdminConfirmPage, nomad.JobAdminConfirmPage: // always make user go down one to confirm m.getCurrentPageModel().SetViewportSelectionToTop() } @@ -337,6 +337,24 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newToast := toast.New(toastMsg) m.getCurrentPageModel().SetToast(newToast, toastStyle) cmds = append(cmds, tea.Tick(newToast.Timeout, func(t time.Time) tea.Msg { return toast.TimeoutMsg{ID: newToast.ID} })) + + case nomad.JobAdminActionCompleteMsg: + toastMsg := fmt.Sprintf( + "%s completed successfully", + nomad.GetJobAdminText(m.adminAction, msg.JobID), + ) + toastStyle := style.SuccessToast + if msg.Err != nil { + toastMsg = fmt.Sprintf( + "%s failed with error: %s", + nomad.GetJobAdminText(m.adminAction, msg.JobID), + msg.Err.Error(), + ) + toastStyle = style.ErrorToast + } + newToast := toast.New(toastMsg) + m.getCurrentPageModel().SetToast(newToast, toastStyle) + cmds = append(cmds, tea.Tick(newToast.Timeout, func(t time.Time) tea.Msg { return toast.TimeoutMsg{ID: newToast.ID} })) } currentPageModel = m.getCurrentPageModel() @@ -444,6 +462,21 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { cmds = append(cmds, m.getCurrentPageCmd()) return tea.Batch(cmds...) } + case nomad.JobAdminPage: + m.adminAction = nomad.KeyToAdminAction(selectedPageRow.Key) + case nomad.JobAdminConfirmPage: + if selectedPageRow.Key == constants.ConfirmationKey { + cmds = append( + cmds, + nomad.GetCmdForJobAdminAction( + m.client, m.adminAction, m.jobID, m.jobNamespace), + ) + } else { + backPage := m.currentPage.Backward(m.inJobsMode) + m.setPage(backPage) + cmds = append(cmds, m.getCurrentPageCmd()) + return tea.Batch(cmds...) + } default: if m.currentPage.ShowsTasks() { taskInfo, err := nomad.TaskInfoFromKey(selectedPageRow.Key) @@ -595,16 +628,26 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { if key.Matches(msg, keymap.KeyMap.AdminMenu) && m.currentPage.HasAdminMenu() { if selectedPageRow, err := m.getCurrentPageModel().GetSelectedPageRow(); err == nil { // Get task info from the currently selected row - taskInfo, err := nomad.TaskInfoFromKey(selectedPageRow.Key) - if err != nil { - m.err = err - return nil - } - if taskInfo.Running { - m.alloc, m.taskName = taskInfo.Alloc, taskInfo.TaskName - m.setPage(nomad.AllocAdminPage) + + if m.currentPage == nomad.JobsPage { + m.jobID, m.jobNamespace = nomad.JobIDAndNamespaceFromKey(selectedPageRow.Key) + m.setPage(nomad.JobAdminPage) return m.getCurrentPageCmd() } + + if m.currentPage == nomad.JobTasksPage { + + taskInfo, err := nomad.TaskInfoFromKey(selectedPageRow.Key) + if err != nil { + m.err = err + return nil + } + if taskInfo.Running { + m.alloc, m.taskName = taskInfo.Alloc, taskInfo.TaskName + m.setPage(nomad.AllocAdminPage) + return m.getCurrentPageCmd() + } + } } } @@ -758,6 +801,36 @@ func (m Model) getCurrentPageCmd() tea.Cmd { }, } } + case nomad.JobAdminPage: + return func() tea.Msg { + // this does no async work, just constructs the job admin menu + var rows []page.Row + for action := range nomad.JobAdminActions { + rows = append(rows, page.Row{ + Key: nomad.AdminActionToKey(action), + Row: nomad.GetJobAdminText(action, m.jobID), + }) + } + return nomad.PageLoadedMsg{ + Page: nomad.JobAdminPage, + TableHeader: []string{"Available Admin Actions"}, + AllPageRows: rows, + } + } + case nomad.JobAdminConfirmPage: + return func() tea.Msg { + // this does no async work, just constructs the confirmation page + confirmationText := nomad.GetJobAdminText(m.adminAction, m.jobID) + confirmationText = strings.ToLower(confirmationText[:1]) + confirmationText[1:] + return nomad.PageLoadedMsg{ + Page: nomad.JobAdminConfirmPage, + TableHeader: []string{"Are you sure?"}, + AllPageRows: []page.Row{ + {Key: "Cancel", Row: "Cancel"}, + {Key: constants.ConfirmationKey, Row: fmt.Sprintf("Yes, %s", confirmationText)}, + }, + } + } default: panic(fmt.Sprintf("Load command for page:%s not found", m.currentPage)) } diff --git a/internal/tui/nomad/jobadmin.go b/internal/tui/nomad/jobadmin.go new file mode 100644 index 00000000..cad2a86e --- /dev/null +++ b/internal/tui/nomad/jobadmin.go @@ -0,0 +1,61 @@ +package nomad + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/hashicorp/nomad/api" +) + +var ( + JobAdminActions = map[AdminAction]string{ + StopJobAction: "Stop", + StopAndPurgeJobAction: "Stop and purge", + } +) + +type JobAdminActionCompleteMsg struct { + Err error + JobID string +} + +func GetJobAdminText(adminAction AdminAction, jobID string) string { + switch adminAction { + case StopJobAction, StopAndPurgeJobAction: + return fmt.Sprintf( + "%s job %s", + JobAdminActions[adminAction], jobID) + default: + return "" + } +} + +func GetCmdForJobAdminAction( + client api.Client, + adminAction AdminAction, + jobID, jobNamespace string, +) tea.Cmd { + switch adminAction { + case StopJobAction: + return StopJob(client, jobID, jobNamespace, false) + case StopAndPurgeJobAction: + return StopJob(client, jobID, jobNamespace, true) + default: + return nil + } +} + +func StopJob(client api.Client, jobID, jobNamespace string, purge bool) tea.Cmd { + return func() tea.Msg { + opts := &api.WriteOptions{Namespace: jobNamespace} + _, _, err := client.Jobs().Deregister(jobID, purge, opts) + if err != nil { + return JobAdminActionCompleteMsg{ + Err: err, + JobID: jobID, + } + } + + return JobAdminActionCompleteMsg{JobID: jobID} + } +} diff --git a/internal/tui/nomad/pages.go b/internal/tui/nomad/pages.go index c4e4a91b..8a81b2d3 100644 --- a/internal/tui/nomad/pages.go +++ b/internal/tui/nomad/pages.go @@ -38,6 +38,8 @@ const ( StatsPage AllocAdminPage AllocAdminConfirmPage + JobAdminPage + JobAdminConfirmPage ) func GetAllPageConfigs(width, height int, compactTables bool) map[Page]page.Config { @@ -144,6 +146,16 @@ func GetAllPageConfigs(width, height int, compactTables bool) map[Page]page.Conf LoadingString: AllocAdminConfirmPage.LoadingString(), SelectionEnabled: true, WrapText: false, RequestInput: false, }, + JobAdminPage: { + Width: width, Height: height, + LoadingString: JobAdminPage.LoadingString(), + SelectionEnabled: true, WrapText: false, RequestInput: false, + }, + JobAdminConfirmPage: { + Width: width, Height: height, + LoadingString: JobAdminConfirmPage.LoadingString(), + SelectionEnabled: true, WrapText: false, RequestInput: false, + }, } } @@ -170,6 +182,8 @@ func (p Page) DoesReload() bool { ExecCompletePage, AllocAdminPage, AllocAdminConfirmPage, + JobAdminPage, + JobAdminConfirmPage, } for _, noReloadPage := range noReloadPages { if noReloadPage == p { @@ -190,7 +204,7 @@ func (p Page) ShowsTasks() bool { } func (p Page) HasAdminMenu() bool { - adminMenuPages := []Page{AllTasksPage, JobTasksPage} + adminMenuPages := []Page{AllTasksPage, JobTasksPage, JobsPage} for _, adminMenuPage := range adminMenuPages { if adminMenuPage == p { return true @@ -219,6 +233,8 @@ func (p Page) doesUpdate() bool { AllEventPage, // doesn't load AllocAdminPage, // doesn't load AllocAdminConfirmPage, // doesn't load + JobAdminPage, // doesn't load + JobAdminConfirmPage, // doesn't load } for _, noUpdatePage := range noUpdatePages { if noUpdatePage == p { @@ -264,7 +280,9 @@ func (p Page) String() string { return "stats" case AllocAdminPage: return "task admin menu" - case AllocAdminConfirmPage: + case JobAdminPage: + return "job admin menu" + case AllocAdminConfirmPage, JobAdminConfirmPage: return "execute" } return "unknown" @@ -294,6 +312,10 @@ func (p Page) Forward(inJobsMode bool) Page { return AllocAdminConfirmPage case AllocAdminConfirmPage: return returnToTasksPage(inJobsMode) + case JobAdminPage: + return JobAdminConfirmPage + case JobAdminConfirmPage: + return JobsPage } return p } @@ -341,6 +363,10 @@ func (p Page) Backward(inJobsMode bool) Page { return returnToTasksPage(inJobsMode) case AllocAdminConfirmPage: return AllocAdminPage + case JobAdminPage: + return JobsPage + case JobAdminConfirmPage: + return JobAdminPage } return p } @@ -400,6 +426,10 @@ func (p Page) GetFilterPrefix(namespace, jobID, taskName, allocName, allocID str return fmt.Sprintf("Admin Actions for Allocation %s %s", style.Bold.Render(allocName), formatter.ShortAllocID(allocID)) case AllocAdminConfirmPage: return fmt.Sprintf("Confirm Admin Action for Allocation %s %s", style.Bold.Render(allocName), formatter.ShortAllocID(allocID)) + case JobAdminPage: + return fmt.Sprintf("Admin Actions for Job %s", style.Bold.Render(jobID)) + case JobAdminConfirmPage: + return fmt.Sprintf("Confirm Admin Action for Job %s", style.Bold.Render(jobID)) default: panic("page not found") } diff --git a/internal/tui/nomad/util.go b/internal/tui/nomad/util.go index 66f50d37..adae6b0c 100644 --- a/internal/tui/nomad/util.go +++ b/internal/tui/nomad/util.go @@ -22,9 +22,10 @@ const ( StopAllocAction RestartJobAction StopJobAction + StopAndPurgeJobAction ) -// AdminActionToKey and KeyToAdminAction are used to render the AllocAdminPage +// AdminActionToKey and KeyToAdminAction are used for admin menu serialization/deserialization func AdminActionToKey(adminAction AdminAction) string { switch adminAction { case RestartTaskAction: @@ -37,6 +38,8 @@ func AdminActionToKey(adminAction AdminAction) string { return "restart-job" case StopJobAction: return "stop-job" + case StopAndPurgeJobAction: + return "stop-and-purge-job" default: return "" } @@ -54,6 +57,8 @@ func KeyToAdminAction(adminAction string) AdminAction { return RestartJobAction case "stop-job": return StopJobAction + case "stop-and-purge-job": + return StopAndPurgeJobAction default: return -1 }