Skip to content

Commit

Permalink
Moar
Browse files Browse the repository at this point in the history
  • Loading branch information
robinovitch61 committed Jul 10, 2022
1 parent 7514ce1 commit c7b6b81
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 44 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -78,6 +78,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 '' on * 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
41 changes: 20 additions & 21 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 @@ -43,24 +47,19 @@ type Model struct {
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 @@ -279,7 +278,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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:
Expand All @@ -304,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 Down Expand Up @@ -434,21 +433,21 @@ 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.nomadUrl, m.nomadToken)
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 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.PrettifyLine(m.logline, nomad.LoglinePage)
default:
Expand Down Expand Up @@ -516,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
19 changes: 11 additions & 8 deletions internal/tui/nomad/events.go
Expand Up @@ -13,15 +13,13 @@ type EventsStreamMsg struct {
Closed bool
}

func FetchEventsStream(url, token string) tea.Cmd {
func FetchEventsStream(url, token, topics, namespace string) tea.Cmd {
return func() tea.Msg {
params := [][2]string{
{"namespace", "default"},
{"topic", "Allocation"},
{"topic", "Job"},
{"topic", "Deployment"},
{"topic", "Evaluation"},
{"index", "0"},
{"namespace", namespace},
}
for _, t := range strings.Split(topics, ",") {
params = append(params, [2]string{"topic", strings.TrimSpace(t)})
}
fullPath := fmt.Sprintf("%s%s", url, "/v1/event/stream")
resp, err := doQuery(fullPath, token, params)
Expand All @@ -32,7 +30,7 @@ func FetchEventsStream(url, token string) tea.Cmd {

peek, err := reader.Peek(17)
if err == nil && string(peek) == "Permission denied" {
return message.ErrMsg{Err: fmt.Errorf("token cannot access topic set %s", "TODO LEO")}
return message.ErrMsg{Err: fmt.Errorf("token not authorized to access topic set\n%s\nin namespace %s", formatEventTopics(topics), namespace)}
}

return PageLoadedMsg{Page: EventsPage, Connection: PersistentConnection{Reader: reader, Body: resp.Body}}
Expand All @@ -52,3 +50,8 @@ func ReadEventsStreamNextMessage(r *bufio.Reader) tea.Cmd {
return EventsStreamMsg{Value: trimmed}
}
}

func formatEventTopics(topics string) string {
noSpaces := strings.ReplaceAll(topics, " ", "")
return strings.ReplaceAll(noSpaces, ",", ", ")
}
4 changes: 4 additions & 0 deletions internal/tui/nomad/jobs.go
Expand Up @@ -2,6 +2,7 @@ package nomad

import (
"encoding/json"
"errors"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/robinovitch61/wander/internal/tui/components/page"
Expand Down Expand Up @@ -61,6 +62,9 @@ func FetchJobs(url, token string) tea.Cmd {
if err != nil {
return message.ErrMsg{Err: err}
}
if strings.Contains(string(body), "UUID must be 36 characters") {
return message.ErrMsg{Err: errors.New("token must be 36 characters")}
}

var jobResponse []jobResponseEntry
if err := json.Unmarshal(body, &jobResponse); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/nomad/pages.go
Expand Up @@ -134,14 +134,14 @@ func (p Page) Backward() Page {
return p
}

func (p Page) GetFilterPrefix(jobID, taskName, allocID string) string {
func (p Page) GetFilterPrefix(jobID, taskName, allocID, eventTopics, eventNamespace string) string {
switch p {
case JobsPage:
return "Jobs"
case JobSpecPage:
return fmt.Sprintf("Job Spec for %s", style.Bold.Render(jobID))
case EventsPage:
return fmt.Sprintf("Global Event Stream")
return fmt.Sprintf("Events in %s for %s", eventNamespace, formatEventTopics(eventTopics))
case EventPage:
return fmt.Sprintf("Event")
case AllocationsPage:
Expand Down

0 comments on commit c7b6b81

Please sign in to comment.