Skip to content

Commit

Permalink
tmp
Browse files Browse the repository at this point in the history
  • Loading branch information
robinovitch61 committed Jul 10, 2022
1 parent 7ab28c0 commit 693c1f4
Show file tree
Hide file tree
Showing 18 changed files with 324 additions and 102 deletions.
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -16,6 +16,10 @@

![wander](./img/logs.png)

### Tail events

![wander](./img/events.png)

### Save any view as a local file

![wander](./img/save.png)
Expand Down Expand Up @@ -78,6 +82,12 @@ nomad_token: <your-nomad-token>
# Seconds between updates for job & allocation pages. Disable with '-1'. Default '2'
wander_update_seconds: 1

# Topics to follow in event stream, comma-separated. Default 'Job,Allocation,Deployment,Evaluation'
wander_event_topics: Job:myjob,Allocation:myjob,Evaluation # see https://www.nomadproject.io/api-docs/events#event-stream for details

# Namespace in event stream. '*' for all namespaces. Default 'default'
wander_event_namespace: '*' # * needs surrounding '' in yaml

# For `wander serve`. Hostname of the machine hosting the ssh server. Default 'localhost'
wander_host: localhost

Expand Down
20 changes: 16 additions & 4 deletions cmd/root.go
Expand Up @@ -47,6 +47,16 @@ var (
cliLong: "update",
config: "wander_update_seconds",
}
eventTopicsArg = arg{
cliShort: "",
cliLong: "event-topics",
config: "wander_event_topics",
}
eventNamespaceArg = arg{
cliShort: "",
cliLong: "event-namespace",
config: "wander_event_namespace",
}

description = `wander is a terminal application for Nomad by HashiCorp. It is used to
view jobs, allocations, tasks, logs, and more, all from the terminal
Expand Down Expand Up @@ -80,6 +90,10 @@ func init() {
viper.BindPFlag(addrArg.cliLong, rootCmd.PersistentFlags().Lookup(addrArg.config))
rootCmd.PersistentFlags().StringP(updateSecondsArg.cliLong, updateSecondsArg.cliShort, "", "Seconds between updates for job & allocation pages. Disable with '-1'. Default '2'")
viper.BindPFlag(updateSecondsArg.cliLong, rootCmd.PersistentFlags().Lookup(updateSecondsArg.config))
rootCmd.PersistentFlags().StringP(eventTopicsArg.cliLong, eventTopicsArg.cliShort, "", "Topics to follow in event stream, comma-separated. Default 'Job,Allocation,Deployment,Evaluation'")
viper.BindPFlag(eventTopicsArg.cliLong, rootCmd.PersistentFlags().Lookup(eventTopicsArg.config))
rootCmd.PersistentFlags().StringP(eventNamespaceArg.cliLong, eventNamespaceArg.cliShort, "", "Namespace in event stream. '*' for all namespaces. Default 'default'")
viper.BindPFlag(eventNamespaceArg.cliLong, rootCmd.PersistentFlags().Lookup(eventNamespaceArg.config))

// serve
serveCmd.PersistentFlags().StringP(hostArg.cliLong, hostArg.cliShort, "", "Host for wander ssh server. Default 'localhost'")
Expand Down Expand Up @@ -123,10 +137,8 @@ func initConfig() {
}

func mainEntrypoint(cmd *cobra.Command, args []string) {
nomadAddr := retrieveAddress(cmd)
nomadToken := retrieveToken(cmd)
updateSeconds := retrieveUpdateSeconds(cmd)
program := tea.NewProgram(initialModel(nomadAddr, nomadToken, updateSeconds), tea.WithAltScreen())
initialModel, options := setup(cmd, "")
program := tea.NewProgram(initialModel, options...)

dev.Debug("~STARTING UP~")
if err := program.Start(); err != nil {
Expand Down
8 changes: 3 additions & 5 deletions cmd/serve.go
Expand Up @@ -79,13 +79,11 @@ func serveEntrypoint(cmd *cobra.Command, args []string) {

func generateTeaHandler(cmd *cobra.Command) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
nomadAddr := retrieveAddress(cmd)
nomadToken := retrieveToken(cmd)
updateSeconds := retrieveUpdateSeconds(cmd)
// optionally override token - MUST run with `-t` flag to force pty, e.g. ssh -p 20000 localhost -t <token>
var overrideToken string
if sshCommands := s.Command(); len(sshCommands) == 1 {
nomadToken = strings.TrimSpace(sshCommands[0])
overrideToken = strings.TrimSpace(sshCommands[0])
}
return initialModel(nomadAddr, nomadToken, updateSeconds), []tea.ProgramOption{tea.WithAltScreen()}
return setup(cmd, overrideToken)
}
}
49 changes: 45 additions & 4 deletions cmd/util.go
@@ -1,7 +1,9 @@
package cmd

import (
"errors"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/wish"
"github.com/gliderlabs/ssh"
"github.com/robinovitch61/wander/internal/tui/components/app"
Expand All @@ -24,6 +26,13 @@ var (
CommitSHA = ""
)

func validateToken(token string) error {
if len(token) > 0 && len(token) != 36 {
return errors.New("token must be 36 characters")
}
return nil
}

func retrieve(cmd *cobra.Command, a arg) (string, error) {
val := cmd.Flag(a.cliLong).Value.String()
if val == "" {
Expand Down Expand Up @@ -72,13 +81,22 @@ func retrieveToken(cmd *cobra.Command) string {
if err != nil {
return ""
}
if len(val) > 0 && len(val) != 36 {
fmt.Println("token must be 36 characters")
err = validateToken(val)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
return val
}

func retrieveEventTopics(cmd *cobra.Command) string {
return retrieveWithDefault(cmd, eventTopicsArg, "Job,Allocation,Deployment,Evaluation")
}

func retrieveEventNamespace(cmd *cobra.Command) string {
return retrieveWithDefault(cmd, eventNamespaceArg, "default")
}

func retrieveUpdateSeconds(cmd *cobra.Command) int {
updateSecondsString := retrieveWithDefault(cmd, updateSecondsArg, "2")
updateSeconds, err := strconv.Atoi(updateSecondsString)
Expand Down Expand Up @@ -106,8 +124,31 @@ func CustomLoggingMiddleware() wish.Middleware {
}
}

func initialModel(addr, token string, updateSeconds int) app.Model {
return app.InitialModel(Version, CommitSHA, addr, token, updateSeconds)
func setup(cmd *cobra.Command, overrideToken string) (app.Model, []tea.ProgramOption) {
nomadAddr := retrieveAddress(cmd)
nomadToken := retrieveToken(cmd)
if overrideToken != "" {
err := validateToken(overrideToken)
if err != nil {
fmt.Println(err.Error())
}
nomadToken = overrideToken
}
eventTopics := retrieveEventTopics(cmd)
eventNamespace := retrieveEventNamespace(cmd)
updateSeconds := retrieveUpdateSeconds(cmd)

initialModel := app.InitialModel(app.Config{
Version: Version,
SHA: CommitSHA,
URL: nomadAddr,
Token: nomadToken,
EventTopics: eventTopics,
EventNamespace: eventNamespace,
UpdateSeconds: time.Second * time.Duration(updateSeconds),
})

return initialModel, []tea.ProgramOption{tea.WithAltScreen()}
}

func getVersion() string {
Expand Down
Binary file added img/events.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 72 additions & 26 deletions internal/tui/components/app/app.go
Expand Up @@ -19,9 +19,13 @@ import (
"time"
)

type Config struct {
Version, SHA, URL, Token, EventTopics, EventNamespace string
UpdateSeconds time.Duration
}

type Model struct {
nomadUrl string
nomadToken string
config Config

header header.Model
currentPage nomad.Page
Expand All @@ -34,30 +38,28 @@ type Model struct {
logline string
logType nomad.LogType

eventsStream nomad.PersistentConnection
event string

execWebSocket *websocket.Conn
execPty *os.File
inPty bool
webSocketConnected bool
lastCommandFinished struct{ stdOut, stdErr bool }

updateInterval time.Duration

width, height int
initialized bool
err error
}

func InitialModel(version, sha, url, token string, updateSeconds int) Model {
func InitialModel(c Config) Model {
firstPage := nomad.JobsPage
initialHeader := header.New(constants.LogoString, url, getVersionString(version, sha), nomad.GetPageKeyHelp(firstPage, false, false, false, false, false, false))
updateInterval := time.Second * time.Duration(updateSeconds)
initialHeader := header.New(constants.LogoString, c.URL, getVersionString(c.Version, c.SHA), nomad.GetPageKeyHelp(firstPage, false, false, false, false, false, false))

return Model{
nomadUrl: url,
nomadToken: token,
header: initialHeader,
currentPage: firstPage,
updateInterval: updateInterval,
config: c,
header: initialHeader,
currentPage: firstPage,
}
}

Expand Down Expand Up @@ -116,6 +118,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.currentPage {
case nomad.JobsPage:
m.jobID, m.jobNamespace = nomad.JobIDAndNamespaceFromKey(selectedPageRow.Key)
case nomad.EventsPage:
m.event = selectedPageRow.Row
case nomad.AllocationsPage:
allocInfo, err := nomad.AllocationInfoFromKey(selectedPageRow.Key)
if err != nil {
Expand Down Expand Up @@ -196,6 +200,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}

if key.Matches(msg, keymap.KeyMap.Events) && m.currentPage == nomad.JobsPage {
m.setPage(nomad.EventsPage)
return m, m.getCurrentPageCmd()
}

if m.currentPage == nomad.LogsPage {
switch {
case key.Matches(msg, keymap.KeyMap.StdOut):
Expand Down Expand Up @@ -238,28 +247,52 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case nomad.PageLoadedMsg:
if msg.Page == m.currentPage {
m.getCurrentPageModel().SetHeader(msg.TableHeader)
m.getCurrentPageModel().SetAllPageData(msg.AllPageData)
m.getCurrentPageModel().SetAllPageData(msg.AllPageRows)
if m.currentPageLoading() {
m.getCurrentPageModel().SetViewportXOffset(0)
}
m.getCurrentPageModel().SetLoading(false)

switch m.currentPage {
case nomad.JobsPage:
if m.currentPage == nomad.JobsPage && len(msg.AllPageData) == 0 {
if m.currentPage == nomad.JobsPage && len(msg.AllPageRows) == 0 {
// oddly, nomad http api errors when one provides the wrong token, but returns empty results when one provides an empty token
m.getCurrentPageModel().SetAllPageData([]page.Row{
{"", "No job results. Is the cluster empty or no nomad token provided?"},
{"", "Press q or ctrl+c to quit."},
})
m.getCurrentPageModel().SetViewportSelectionEnabled(false)
}
case nomad.EventsPage:
if m.eventsStream.Body != nil {
err := m.eventsStream.Body.Close()
if err != nil {
m.err = err
return m, nil
}
}
m.eventsStream = msg.Connection
cmds = append(cmds, nomad.ReadEventsStreamNextMessage(m.eventsStream.Reader))
case nomad.LogsPage:
m.getCurrentPageModel().SetViewportSelectionToBottom()
case nomad.ExecPage:
m.getCurrentPageModel().SetInputPrefix("Enter command: ")
}
cmds = append(cmds, nomad.UpdatePageDataWithDelay(m.currentPage, m.updateInterval))
cmds = append(cmds, nomad.UpdatePageDataWithDelay(m.currentPage, m.config.UpdateSeconds))
}

case nomad.EventsStreamMsg:
if m.currentPage == nomad.EventsPage {
if !msg.Closed {
if msg.Value != "{}" {
scrollDown := m.getCurrentPageModel().ViewportSelectionAtBottom()
m.getCurrentPageModel().AppendToViewport([]page.Row{{Row: msg.Value}}, true)
if scrollDown {
m.getCurrentPageModel().ScrollViewportToBottom()
}
}
cmds = append(cmds, nomad.ReadEventsStreamNextMessage(m.eventsStream.Reader))
}
}

case nomad.UpdatePageDataMsg:
Expand All @@ -270,7 +303,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case message.PageInputReceivedMsg:
if m.currentPage == nomad.ExecPage {
m.getCurrentPageModel().SetLoading(true)
return m, nomad.InitiateWebSocket(m.nomadUrl, m.nomadToken, m.allocID, m.taskName, msg.Input)
return m, nomad.InitiateWebSocket(m.config.URL, m.config.Token, m.allocID, m.taskName, msg.Input)
}

case nomad.ExecWebSocketConnectedMsg:
Expand All @@ -297,6 +330,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.webSocketConnected = false
m.setInPty(false)
m.getCurrentPageModel().AppendToViewport([]page.Row{{Row: constants.ExecWebSocketClosed}}, true)
m.getCurrentPageModel().ScrollViewportToBottom()
} else {
m.appendToViewport(msg.StdOut, m.lastCommandFinished.stdOut)
m.appendToViewport(msg.StdErr, m.lastCommandFinished.stdErr)
Expand Down Expand Up @@ -339,6 +373,12 @@ func (m *Model) initialize() {
jobSpecPage := page.New(m.width, pageHeight, m.getFilterPrefix(nomad.JobSpecPage), nomad.JobSpecPage.LoadingString(), false, true, false, nil)
m.pageModels[nomad.JobSpecPage] = &jobSpecPage

eventsPage := page.New(m.width, pageHeight, m.getFilterPrefix(nomad.EventsPage), nomad.EventsPage.LoadingString(), true, false, false, nil)
m.pageModels[nomad.EventsPage] = &eventsPage

eventPage := page.New(m.width, pageHeight, m.getFilterPrefix(nomad.EventPage), nomad.EventPage.LoadingString(), false, true, false, nil)
m.pageModels[nomad.EventPage] = &eventPage

allocationsPage := page.New(m.width, pageHeight, m.getFilterPrefix(nomad.AllocationsPage), nomad.AllocationsPage.LoadingString(), true, false, false, constants.AllocationsViewportConditionalStyle)
m.pageModels[nomad.AllocationsPage] = &allocationsPage

Expand All @@ -359,6 +399,9 @@ func (m *Model) initialize() {

func (m *Model) cleanupCmd() tea.Cmd {
return func() tea.Msg {
if m.eventsStream.Body != nil {
_ = m.eventsStream.Body.Close()
}
if m.execWebSocket != nil {
nomad.CloseWebSocket(m.execWebSocket)()
}
Expand Down Expand Up @@ -390,21 +433,23 @@ func (m *Model) getCurrentPageModel() *page.Model {
func (m *Model) getCurrentPageCmd() tea.Cmd {
switch m.currentPage {
case nomad.JobsPage:
return nomad.FetchJobs(m.nomadUrl, m.nomadToken)
return nomad.FetchJobs(m.config.URL, m.config.Token)
case nomad.JobSpecPage:
return nomad.FetchJobSpec(m.nomadUrl, m.nomadToken, m.jobID, m.jobNamespace)
return nomad.FetchJobSpec(m.config.URL, m.config.Token, m.jobID, m.jobNamespace)
case nomad.EventsPage:
return nomad.FetchEventsStream(m.config.URL, m.config.Token, m.config.EventTopics, m.config.EventNamespace)
case nomad.EventPage:
return nomad.PrettifyLine(m.event, nomad.EventPage)
case nomad.AllocationsPage:
return nomad.FetchAllocations(m.nomadUrl, m.nomadToken, m.jobID, m.jobNamespace)
return nomad.FetchAllocations(m.config.URL, m.config.Token, m.jobID, m.jobNamespace)
case nomad.ExecPage:
return func() tea.Msg {
return nomad.PageLoadedMsg{Page: nomad.ExecPage, TableHeader: []string{}, AllPageData: []page.Row{}}
}
return nomad.LoadExecPage()
case nomad.AllocSpecPage:
return nomad.FetchAllocSpec(m.nomadUrl, m.nomadToken, m.allocID)
return nomad.FetchAllocSpec(m.config.URL, m.config.Token, m.allocID)
case nomad.LogsPage:
return nomad.FetchLogs(m.nomadUrl, m.nomadToken, m.allocID, m.taskName, m.logType)
return nomad.FetchLogs(m.config.URL, m.config.Token, m.allocID, m.taskName, m.logType)
case nomad.LoglinePage:
return nomad.FetchLogLine(m.logline)
return nomad.PrettifyLine(m.logline, nomad.LoglinePage)
default:
panic("page load command not found")
}
Expand All @@ -418,6 +463,7 @@ func (m *Model) appendToViewport(content string, startOnNewLine bool) {
pageRows = append(pageRows, page.Row{Row: stripped})
}
m.getCurrentPageModel().AppendToViewport(pageRows, startOnNewLine)
m.getCurrentPageModel().ScrollViewportToBottom()
}

// updateLastCommandFinished updates lastCommandFinished, which is necessary
Expand Down Expand Up @@ -469,7 +515,7 @@ func (m Model) currentPageViewportSaving() bool {
}

func (m Model) getFilterPrefix(page nomad.Page) string {
return page.GetFilterPrefix(m.jobID, m.taskName, m.allocID)
return page.GetFilterPrefix(m.jobID, m.taskName, m.allocID, m.config.EventTopics, m.config.EventNamespace)
}

func getVersionString(v, s string) string {
Expand Down

0 comments on commit 693c1f4

Please sign in to comment.