With nothing installed, sudo llmgw asks for one machine or three, takes how to reach the other two (ssh agent, key or password, and a sudo password used only for the install), checks every machine first, shows the plan and its command line, and installs. On a control plane it opens a console with the cluster's state and the next steps. Machines are reached with Go's ssh so a password works; the store's etcd and access rules use this machine's routed address rather than a client-facing name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
120 lines
3.7 KiB
Go
120 lines
3.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/preflight"
|
|
)
|
|
|
|
func press(m tea.Model, keys ...string) tea.Model {
|
|
for _, k := range keys {
|
|
var msg tea.KeyMsg
|
|
switch k {
|
|
case "enter":
|
|
msg = tea.KeyMsg{Type: tea.KeyEnter}
|
|
case "tab":
|
|
msg = tea.KeyMsg{Type: tea.KeyTab}
|
|
case "down":
|
|
msg = tea.KeyMsg{Type: tea.KeyDown}
|
|
case "esc":
|
|
msg = tea.KeyMsg{Type: tea.KeyEsc}
|
|
case "right":
|
|
msg = tea.KeyMsg{Type: tea.KeyRight}
|
|
default:
|
|
msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(k)}
|
|
}
|
|
m, _ = m.Update(msg)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestTheSetupAsksForWhatHighAvailabilityNeedsAndRefusesWhatWillNotDo(t *testing.T) {
|
|
m := tea.Model(newSetupModel("ctl", []string{"10.0.0.5"}))
|
|
m = press(m, "down", "enter")
|
|
if s := m.(setupModel); s.page != pAdmin {
|
|
t.Fatalf("high availability did not lead to the administrator: page %d", s.page)
|
|
}
|
|
// No email: refused, with a reason.
|
|
m = press(m, "tab", "tab", "enter")
|
|
if s := m.(setupModel); s.page != pAdmin || !strings.Contains(s.formErr, "email") {
|
|
t.Fatalf("went on without an administrator: %+v", s.formErr)
|
|
}
|
|
// The refusal puts the cursor back on the email.
|
|
m = press(m, "a@example.com", "tab", "tab", "enter")
|
|
s := m.(setupModel)
|
|
if s.page != pMachines {
|
|
t.Fatalf("page %d: %s", s.page, s.formErr)
|
|
}
|
|
// This machine's own address is not a database machine.
|
|
s.db[0].addr.SetValue("10.0.0.5")
|
|
s.db[0].user.SetValue("ubuntu")
|
|
s.db[0].auth = authAgent
|
|
s.db[1].addr.SetValue("10.0.0.7")
|
|
s.db[1].user.SetValue("ubuntu")
|
|
s.db[1].auth = authPassword
|
|
if err := s.validateMachines(); !strings.Contains(err, "this machine") {
|
|
t.Errorf("this machine as a database machine: %q", err)
|
|
}
|
|
s.db[0].addr.SetValue("10.0.0.6")
|
|
if err := s.validateMachines(); !strings.Contains(err, "password") {
|
|
t.Errorf("a password login with no password: %q", err)
|
|
}
|
|
s.db[1].secret.SetValue("pw")
|
|
if err := s.validateMachines(); err != "" {
|
|
t.Errorf("a valid pair refused: %s", err)
|
|
}
|
|
cmd := s.command()
|
|
for _, want := range []string{"--admin a@example.com", "--patroni ubuntu@10.0.0.6", "--patroni ubuntu@10.0.0.7", "--no-node"} {
|
|
if !strings.Contains(cmd, want) {
|
|
t.Errorf("command lacks %q:\n%s", want, cmd)
|
|
}
|
|
}
|
|
if strings.Contains(cmd, "pw") {
|
|
t.Error("the password is in the command line")
|
|
}
|
|
}
|
|
|
|
func TestChoosingOneMachineHandsOverToInit(t *testing.T) {
|
|
m := press(tea.Model(newSetupModel("ctl", nil)), "enter").(setupModel)
|
|
if !m.single {
|
|
t.Error("one machine did not hand over")
|
|
}
|
|
}
|
|
|
|
func TestTheChecksHoldTheSetupBackOnlyForAFailure(t *testing.T) {
|
|
m := newSetupModel("ctl", nil)
|
|
m.choice, m.page = 1, pCheck
|
|
m.report = preflight.Report{Findings: []preflight.Finding{{Machine: "a", Check: "disk", Level: preflight.Fail, Fix: "more"}}}
|
|
if s := press(m, "enter").(setupModel); s.page != pCheck {
|
|
t.Error("went on past a failed check")
|
|
}
|
|
m.report.Findings[0].Level = preflight.Warn
|
|
if s := press(m, "enter").(setupModel); s.page != pPlan {
|
|
t.Error("a warning held it back")
|
|
}
|
|
}
|
|
|
|
func TestTheConsoleOffersToReplicateOnlyAStoreThatIsNot(t *testing.T) {
|
|
m := consoleModel{}
|
|
if !strings.Contains(m.items()[1].label, "Replicate") {
|
|
t.Error("a single store is not offered replication")
|
|
}
|
|
m.store = true
|
|
for _, it := range m.items() {
|
|
if strings.Contains(it.label, "Replicate") {
|
|
t.Error("a replicated store is offered replication")
|
|
}
|
|
}
|
|
// A node is asked for, then the console closes with it to do.
|
|
var mm tea.Model = consoleModel{summary: "x", loaded: true}
|
|
mm = press(mm, "enter", "ubuntu@10.0.0.9", "enter")
|
|
if a := mm.(consoleModel).action; a.kind != "node" || a.targets[0] != "ubuntu@10.0.0.9" {
|
|
t.Errorf("%+v", a)
|
|
}
|
|
_ = adminapi.Summary{}
|
|
}
|