fix: tunnel credentials now use plain terminal prompts
Bubbletea and huh were fighting over terminal input. Fixed by: - TUI exits when user selects 'Remote Assistance' - Plain terminal prompts in main.go collect jumphost URL, user, pass - Starts tunnel directly, shows connection string for admin - No more conflicting rendering between TUI and forms
This commit is contained in:
parent
c169887fed
commit
8b610b1bdb
2 changed files with 79 additions and 121 deletions
|
|
@ -10,23 +10,84 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/ui"
|
||||
)
|
||||
|
||||
// Embedded tunnel key — set at build time with:
|
||||
//
|
||||
// go build -ldflags "-X main.tunnelKey=$(cat ~/.wallarm/tunnel_key)"
|
||||
// Embedded tunnel key for --tunnel flag (set at build time with -ldflags).
|
||||
var tunnelKey string
|
||||
|
||||
// Version is set at build time with -ldflags "-X main.version=1.0.0"
|
||||
// Version set at build time.
|
||||
var version = "dev"
|
||||
|
||||
// runTunnelFlow prompts for tunnel credentials in plain terminal and starts the tunnel.
|
||||
func runTunnelFlow() {
|
||||
fmt.Println()
|
||||
fmt.Println("🔗 Remote Assistance — Secure Tunnel Setup")
|
||||
fmt.Println()
|
||||
fmt.Println("The tunnel lets a remote admin connect to this server through your jumphost.")
|
||||
fmt.Println("Enter the SSH endpoint where the admin will connect:")
|
||||
fmt.Println()
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Jumphost URL [ssh.sechpoint.app:443]: ")
|
||||
host, _ := reader.ReadString('\n')
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
host = "ssh.sechpoint.app:443"
|
||||
}
|
||||
|
||||
fmt.Print("Username [wallarm-tunnel]: ")
|
||||
user, _ := reader.ReadString('\n')
|
||||
user = strings.TrimSpace(user)
|
||||
if user == "" {
|
||||
user = "wallarm-tunnel"
|
||||
}
|
||||
|
||||
fmt.Print("Password or SSH key path: ")
|
||||
pass, _ := reader.ReadString('\n')
|
||||
pass = strings.TrimSpace(pass)
|
||||
if pass == "" {
|
||||
fmt.Println("No credentials provided. Aborting.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Starting tunnel...")
|
||||
|
||||
cfg := tunnel.Config{
|
||||
Jumphost: host,
|
||||
User: user,
|
||||
RemotePort: 9042,
|
||||
LocalSSHPort: 22,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(pass, "/") {
|
||||
cfg.KeyPath = pass
|
||||
} else {
|
||||
cfg.Password = pass
|
||||
}
|
||||
|
||||
fmt.Println("Share this with your admin:")
|
||||
fmt.Println()
|
||||
fmt.Printf(" ssh -p 9042 %s@%s\n", user, host)
|
||||
fmt.Println()
|
||||
fmt.Println("Press Ctrl+C to close the tunnel.")
|
||||
|
||||
if err := tunnel.Start(cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Tunnel error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, `wallarm — Wallarm Deployment Manager
|
||||
|
|
@ -95,8 +156,12 @@ deployment. On subsequent runs, it shows your existing deployments.
|
|||
fmt.Println()
|
||||
|
||||
// 2. Launch bubbletea TUI (handles wizard vs dashboard routing)
|
||||
if err := ui.Run(result); err != nil {
|
||||
err := ui.Run(result)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 3. If TUI exited cleanly (user chose Remote Assistance), start tunnel flow
|
||||
runTunnelFlow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import (
|
|||
"git.sechpoint.app/customer-engineering/wallarm/internal/native"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
|
||||
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
|
||||
)
|
||||
|
||||
// Styles
|
||||
|
|
@ -30,14 +29,11 @@ var (
|
|||
|
||||
// Model is the top-level bubbletea model.
|
||||
type Model struct {
|
||||
state stateView
|
||||
width int
|
||||
height int
|
||||
status string // feedback message during deployment
|
||||
deploying bool // true while deployment is in progress
|
||||
tunnelUser string // tunnel credentials (collected in remote config)
|
||||
tunnelPass string
|
||||
tunnelHost string
|
||||
state stateView
|
||||
width int
|
||||
height int
|
||||
status string // feedback message during deployment
|
||||
deploying bool // true while deployment is in progress
|
||||
}
|
||||
|
||||
type stateView int
|
||||
|
|
@ -45,9 +41,7 @@ type stateView int
|
|||
const (
|
||||
viewPreflight stateView = iota
|
||||
viewRoute
|
||||
viewDeployChoice // new: local deploy or remote assist?
|
||||
viewRemoteConfig // new: prompt for tunnel credentials
|
||||
viewTunnelActive // new: tunnel is running, waiting for remote admin
|
||||
viewDeployChoice // local deploy or remote assist?
|
||||
viewWizard
|
||||
viewDashboard
|
||||
viewDone
|
||||
|
|
@ -90,15 +84,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
case "2":
|
||||
if m.state == viewDeployChoice {
|
||||
m.state = viewRemoteConfig
|
||||
return m, nil
|
||||
}
|
||||
case "s":
|
||||
if m.state == viewRemoteConfig && m.tunnelHost == "" {
|
||||
return m, m.collectTunnelCreds()
|
||||
}
|
||||
if m.state == viewRemoteConfig && m.tunnelHost != "" {
|
||||
return m, m.startTunnel()
|
||||
return m, func() tea.Msg { return tea.Quit() } // exit TUI for tunnel flow
|
||||
}
|
||||
case "enter":
|
||||
if m.state == viewDone || m.state == viewError {
|
||||
|
|
@ -115,14 +101,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.state = viewDone
|
||||
}
|
||||
return m, nil
|
||||
case tunnelCredsMsg:
|
||||
if msg.err != nil {
|
||||
m.status = fmt.Sprintf("Could not collect credentials: %v", msg.err)
|
||||
m.state = viewError
|
||||
} else {
|
||||
m.state = viewRemoteConfig // refresh to show collected creds
|
||||
}
|
||||
return m, nil
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
|
|
@ -145,10 +123,6 @@ func (m Model) View() string {
|
|||
return ""
|
||||
case viewDeployChoice:
|
||||
return deployChoiceView(m)
|
||||
case viewRemoteConfig:
|
||||
return remoteConfigView(m)
|
||||
case viewTunnelActive:
|
||||
return tunnelActiveView(m)
|
||||
case viewWizard:
|
||||
return wizardView(m)
|
||||
case viewDashboard:
|
||||
|
|
@ -161,57 +135,6 @@ func (m Model) View() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// collectTunnelCreds runs a huh form to collect tunnel endpoint details.
|
||||
func (m Model) collectTunnelCreds() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var host, user, pass string
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Jumphost URL").
|
||||
Placeholder("ssh.sechpoint.app:443").
|
||||
Value(&host),
|
||||
huh.NewInput().
|
||||
Title("Username").
|
||||
Placeholder("wallarm-tunnel").
|
||||
Value(&user),
|
||||
huh.NewInput().
|
||||
Title("Password (or SSH key path)").
|
||||
Placeholder("password or /path/to/key").
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Value(&pass),
|
||||
),
|
||||
).WithTheme(huh.ThemeCharm()).Run()
|
||||
|
||||
m.tunnelHost = host
|
||||
m.tunnelUser = user
|
||||
m.tunnelPass = pass
|
||||
return tunnelCredsMsg{err: err}
|
||||
}
|
||||
}
|
||||
|
||||
type tunnelCredsMsg struct{ err error }
|
||||
|
||||
// startTunnel opens the reverse SSH tunnel with collected credentials.
|
||||
func (m Model) startTunnel() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
cfg := tunnel.Config{
|
||||
Jumphost: m.tunnelHost,
|
||||
User: m.tunnelUser,
|
||||
Password: m.tunnelPass,
|
||||
RemotePort: 9042,
|
||||
LocalSSHPort: 22,
|
||||
}
|
||||
// If password looks like a file path, use key auth instead
|
||||
if strings.HasPrefix(m.tunnelPass, "/") {
|
||||
cfg.Password = ""
|
||||
cfg.KeyPath = m.tunnelPass
|
||||
}
|
||||
|
||||
err := tunnel.Start(cfg)
|
||||
return deployCompleteMsg{err: err}
|
||||
}
|
||||
}
|
||||
func (m Model) startDeploy() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
m.deploying = true
|
||||
|
|
@ -246,36 +169,6 @@ func deployChoiceView(m Model) string {
|
|||
return s
|
||||
}
|
||||
|
||||
// ─── Remote Config ───────────────────────────────────────────────────
|
||||
|
||||
func remoteConfigView(m Model) string {
|
||||
s := titleStyle.Render("🔗 Remote Assistance Setup") + "\n\n"
|
||||
s += "Enter the tunnel endpoint where the remote admin will connect:\n\n"
|
||||
|
||||
if m.tunnelHost == "" {
|
||||
s += dimStyle.Render(" (Form will appear below — enter values and press Enter to continue)") + "\n"
|
||||
s += dimStyle.Render(" Press 's' to start the tunnel after filling in credentials") + "\n"
|
||||
} else {
|
||||
s += fmt.Sprintf(" Jumphost: %s\n", goodStyle.Render(m.tunnelHost))
|
||||
s += fmt.Sprintf(" User: %s\n", m.tunnelUser)
|
||||
s += dimStyle.Render("\n Tunnel credentials collected. Press 's' to start.") + "\n"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ─── Tunnel Active ───────────────────────────────────────────────────
|
||||
|
||||
func tunnelActiveView(m Model) string {
|
||||
s := titleStyle.Render("🔗 Tunnel Active") + "\n\n"
|
||||
s += goodStyle.Render("Secure tunnel is running.") + "\n\n"
|
||||
s += "Share this with your admin:\n"
|
||||
s += dimStyle.Render(" ┌─────────────────────────────────────────┐") + "\n"
|
||||
s += fmt.Sprintf(" │ %s", activeStyle.Render("ssh -p 9042 root@"+m.tunnelHost)) + dimStyle.Render(" │") + "\n"
|
||||
s += dimStyle.Render(" └─────────────────────────────────────────┘") + "\n\n"
|
||||
s += dimStyle.Render("The admin will see the deployment wizard on their terminal.") + "\n"
|
||||
s += dimStyle.Render("Tunnel stays open until you press q.") + "\n"
|
||||
return s
|
||||
}
|
||||
|
||||
func wizardView(m Model) string {
|
||||
s := titleStyle.Render("🛡️ Wallarm Setup Wizard") + "\n"
|
||||
|
|
@ -289,7 +182,7 @@ func wizardView(m Model) string {
|
|||
s += activeStyle.Render("Wallarm Native Node Deployment") + "\n"
|
||||
s += " Press Enter to begin configuration" + "\n\n"
|
||||
|
||||
s += activeStyle.Render("Step 2: Cloud region") + "\n"
|
||||
s += activeStyle.Render("Cloud region:") + "\n"
|
||||
r := preflightResult
|
||||
if r.USReachable {
|
||||
s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n"
|
||||
|
|
@ -301,7 +194,7 @@ func wizardView(m Model) string {
|
|||
} else {
|
||||
s += dimStyle.Render(" EU — not reachable") + "\n"
|
||||
}
|
||||
s += dimStyle.Render("\nPress q to quit")
|
||||
s += dimStyle.Render("\nPress Enter to continue, q to quit")
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue