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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
||||||
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
|
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
|
||||||
"git.sechpoint.app/customer-engineering/wallarm/internal/ui"
|
"git.sechpoint.app/customer-engineering/wallarm/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Embedded tunnel key — set at build time with:
|
// Embedded tunnel key for --tunnel flag (set at build time with -ldflags).
|
||||||
//
|
|
||||||
// go build -ldflags "-X main.tunnelKey=$(cat ~/.wallarm/tunnel_key)"
|
|
||||||
var tunnelKey string
|
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"
|
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() {
|
func main() {
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, `wallarm — Wallarm Deployment Manager
|
fmt.Fprintf(os.Stderr, `wallarm — Wallarm Deployment Manager
|
||||||
|
|
@ -95,8 +156,12 @@ deployment. On subsequent runs, it shows your existing deployments.
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// 2. Launch bubbletea TUI (handles wizard vs dashboard routing)
|
// 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)
|
fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
|
||||||
os.Exit(1)
|
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/native"
|
||||||
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
|
||||||
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
|
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
|
||||||
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
|
|
@ -35,9 +34,6 @@ type Model struct {
|
||||||
height int
|
height int
|
||||||
status string // feedback message during deployment
|
status string // feedback message during deployment
|
||||||
deploying bool // true while deployment is in progress
|
deploying bool // true while deployment is in progress
|
||||||
tunnelUser string // tunnel credentials (collected in remote config)
|
|
||||||
tunnelPass string
|
|
||||||
tunnelHost string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type stateView int
|
type stateView int
|
||||||
|
|
@ -45,9 +41,7 @@ type stateView int
|
||||||
const (
|
const (
|
||||||
viewPreflight stateView = iota
|
viewPreflight stateView = iota
|
||||||
viewRoute
|
viewRoute
|
||||||
viewDeployChoice // new: local deploy or remote assist?
|
viewDeployChoice // local deploy or remote assist?
|
||||||
viewRemoteConfig // new: prompt for tunnel credentials
|
|
||||||
viewTunnelActive // new: tunnel is running, waiting for remote admin
|
|
||||||
viewWizard
|
viewWizard
|
||||||
viewDashboard
|
viewDashboard
|
||||||
viewDone
|
viewDone
|
||||||
|
|
@ -90,15 +84,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case "2":
|
case "2":
|
||||||
if m.state == viewDeployChoice {
|
if m.state == viewDeployChoice {
|
||||||
m.state = viewRemoteConfig
|
return m, func() tea.Msg { return tea.Quit() } // exit TUI for tunnel flow
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
case "enter":
|
case "enter":
|
||||||
if m.state == viewDone || m.state == viewError {
|
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
|
m.state = viewDone
|
||||||
}
|
}
|
||||||
return m, nil
|
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:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
|
@ -145,10 +123,6 @@ func (m Model) View() string {
|
||||||
return ""
|
return ""
|
||||||
case viewDeployChoice:
|
case viewDeployChoice:
|
||||||
return deployChoiceView(m)
|
return deployChoiceView(m)
|
||||||
case viewRemoteConfig:
|
|
||||||
return remoteConfigView(m)
|
|
||||||
case viewTunnelActive:
|
|
||||||
return tunnelActiveView(m)
|
|
||||||
case viewWizard:
|
case viewWizard:
|
||||||
return wizardView(m)
|
return wizardView(m)
|
||||||
case viewDashboard:
|
case viewDashboard:
|
||||||
|
|
@ -161,57 +135,6 @@ func (m Model) View() string {
|
||||||
return ""
|
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 {
|
func (m Model) startDeploy() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
m.deploying = true
|
m.deploying = true
|
||||||
|
|
@ -246,36 +169,6 @@ func deployChoiceView(m Model) string {
|
||||||
return s
|
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 {
|
func wizardView(m Model) string {
|
||||||
s := titleStyle.Render("🛡️ Wallarm Setup Wizard") + "\n"
|
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 += activeStyle.Render("Wallarm Native Node Deployment") + "\n"
|
||||||
s += " Press Enter to begin configuration" + "\n\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
|
r := preflightResult
|
||||||
if r.USReachable {
|
if r.USReachable {
|
||||||
s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n"
|
s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n"
|
||||||
|
|
@ -301,7 +194,7 @@ func wizardView(m Model) string {
|
||||||
} else {
|
} else {
|
||||||
s += dimStyle.Render(" EU — not reachable") + "\n"
|
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
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue