blob: 52885bd6d3e0632ce5b85b583e817b59b1e3588d [file] [log] [blame]
Serge Bazanski0d9e1252024-09-03 12:16:47 +02001package tconsole
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "strings"
8
9 "github.com/gdamore/tcell/v2"
10
11 "source.monogon.dev/metropolis/node/core/network"
12 "source.monogon.dev/metropolis/node/core/roleserve"
13 cpb "source.monogon.dev/metropolis/proto/common"
14 "source.monogon.dev/osbase/event"
15 "source.monogon.dev/osbase/supervisor"
16)
17
18// Console is a Terminal Console (TConsole), a user-interactive informational
19// display visible on the TTY of a running Metropolis instance.
20type Console struct {
21 // Quit will be closed when the user press CTRL-C. A new channel will be created
22 // on each New call.
23
24 Quit chan struct{}
25 ttyPath string
26 tty tcell.Tty
27 screen tcell.Screen
28 width int
29 height int
30 // palette chosen for the given terminal.
31 palette palette
32
33 network event.Value[*network.Status]
34 roles event.Value[*cpb.NodeRoles]
35 curatorConn event.Value[*roleserve.CuratorConnection]
36}
37
38// New creates a new Console, taking over the TTY at the given path. The given
39// Terminal type selects between a Linux terminal (VTY) and a generic terminal
40// for testing.
41//
42// network, roles, curatorConn point to various Metropolis subsystems that are
43// used to populate the console data.
44func New(terminal Terminal, ttyPath string, network event.Value[*network.Status], roles event.Value[*cpb.NodeRoles], curatorConn event.Value[*roleserve.CuratorConnection]) (*Console, error) {
45 tty, err := tcell.NewDevTtyFromDev(ttyPath)
46 if err != nil {
47 return nil, err
48 }
49 screen, err := tcell.NewTerminfoScreenFromTty(tty)
50 if err != nil {
51 return nil, err
52 }
53 if err := screen.Init(); err != nil {
54 return nil, err
55 }
56 screen.SetStyle(tcell.StyleDefault)
57
58 var pal palette
59 switch terminal {
60 case TerminalLinux:
61 pal = paletteLinux
62 tty.Write([]byte(pal.setupLinuxConsole()))
63 case TerminalGeneric:
64 pal = paletteGeneric
65 }
66
67 width, height := screen.Size()
68 return &Console{
69 ttyPath: ttyPath,
70 tty: tty,
71 screen: screen,
72 width: width,
73 height: height,
74 network: network,
75 palette: pal,
76 Quit: make(chan struct{}),
77
78 roles: roles,
79 curatorConn: curatorConn,
80 }, nil
81}
82
83// Cleanup should be called when the console exits. This is only used in testing,
84// the Metropolis console always runs.
85func (c *Console) Cleanup() {
86 c.screen.Fini()
87}
88
89func (c *Console) processEvent(ev tcell.Event) {
90 switch ev := ev.(type) {
91 case *tcell.EventKey:
92 if ev.Key() == tcell.KeyCtrlC {
93 close(c.Quit)
94 }
95 case *tcell.EventResize:
96 c.width, c.height = ev.Size()
97 }
98}
99
100// Run blocks while displaying the Console to the user.
101func (c *Console) Run(ctx context.Context) error {
102 // Build channel for console event processing.
103 evC := make(chan tcell.Event)
104 evQuitC := make(chan struct{})
105 defer close(evQuitC)
106 go c.screen.ChannelEvents(evC, evQuitC)
107
108 // Pipe event values into channels.
109 netAddrC := make(chan *network.Status)
110 rolesC := make(chan *cpb.NodeRoles)
111 curatorConnC := make(chan *roleserve.CuratorConnection)
112 if err := supervisor.Run(ctx, "netpipe", event.Pipe(c.network, netAddrC)); err != nil {
113 return err
114 }
115 if err := supervisor.Run(ctx, "rolespipe", event.Pipe(c.roles, rolesC)); err != nil {
116 return err
117 }
118 if err := supervisor.Run(ctx, "curatorpipe", event.Pipe(c.curatorConn, curatorConnC)); err != nil {
119 return err
120 }
121 supervisor.Signal(ctx, supervisor.SignalHealthy)
122
123 pageStatus := pageStatusData{
124 netAddr: "Waiting...",
125 roles: "Waiting...",
126 id: "Waiting...",
127 fingerprint: "Waiting...",
128 }
129
130 for {
131 c.pageStatus(&pageStatus)
132
133 select {
134 case <-ctx.Done():
135 return ctx.Err()
136 case ev := <-evC:
137 c.processEvent(ev)
138 case t := <-netAddrC:
139 pageStatus.netAddr = t.ExternalAddress.String()
140 case t := <-rolesC:
141 var rlist []string
142 if t.ConsensusMember != nil {
143 rlist = append(rlist, "ConsensusMember")
144 }
145 if t.KubernetesController != nil {
146 rlist = append(rlist, "KubernetesController")
147 }
148 if t.KubernetesWorker != nil {
149 rlist = append(rlist, "KubernetesWorker")
150 }
151 pageStatus.roles = strings.Join(rlist, ", ")
152 if pageStatus.roles == "" {
153 pageStatus.roles = "none"
154 }
155 case t := <-curatorConnC:
156 pageStatus.id = t.Credentials.ID()
157 cert := t.Credentials.ClusterCA()
158 sum := sha256.New()
159 sum.Write(cert.Raw)
160 pageStatus.fingerprint = hex.EncodeToString(sum.Sum(nil))
161 }
162 }
163}