334 lines
9 KiB
Go
334 lines
9 KiB
Go
// wallarm — single-binary Wallarm deployment manager.
|
|
//
|
|
// On every start: run preflight checks → detect existing deployment → route to wizard or dashboard.
|
|
//
|
|
// Commands:
|
|
//
|
|
// wallarm Auto-detect state, show wizard or dashboard
|
|
// wallarm --tunnel Start reverse SSH tunnel over TLS:443
|
|
// wallarm --help Show help
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
"git.sechpoint.app/customer-engineering/wallarm/internal/ui"
|
|
)
|
|
|
|
// Embedded tunnel key for --tunnel flag (set at build time with -ldflags).
|
|
var tunnelKey string
|
|
|
|
// Version set at build time.
|
|
var version = "dev"
|
|
|
|
// runDeployFlow runs the deployment wizard in plain terminal (no bubbletea).
|
|
func runDeployFlow(r preflight.Result) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
var s state.State
|
|
s.DeploymentType = "native"
|
|
|
|
// Step 1: Cloud region
|
|
fmt.Println()
|
|
fmt.Println("─── Cloud Region ───")
|
|
if r.USReachable && r.EUReachable {
|
|
fmt.Println(" [1] US (us1.api.wallarm.com)")
|
|
fmt.Println(" [2] EU (api.wallarm.com)")
|
|
fmt.Print("Choose region [1/2]: ")
|
|
choice, _ := reader.ReadString('\n')
|
|
choice = strings.TrimSpace(choice)
|
|
if choice == "2" {
|
|
s.CloudRegion = "EU"
|
|
s.APIHost = "api.wallarm.com"
|
|
} else {
|
|
s.CloudRegion = "US"
|
|
s.APIHost = "us1.api.wallarm.com"
|
|
}
|
|
} else if r.USReachable {
|
|
fmt.Println(" US (us1.api.wallarm.com) — only reachable region")
|
|
s.CloudRegion = "US"
|
|
s.APIHost = "us1.api.wallarm.com"
|
|
} else {
|
|
fmt.Println(" EU (api.wallarm.com) — only reachable region")
|
|
s.CloudRegion = "EU"
|
|
s.APIHost = "api.wallarm.com"
|
|
}
|
|
|
|
// Step 2: API Token
|
|
fmt.Println()
|
|
fmt.Print("Wallarm API Token (Deploy role): ")
|
|
token, _ := reader.ReadString('\n')
|
|
s.APIToken = strings.TrimSpace(token)
|
|
if s.APIToken == "" {
|
|
fmt.Println("Token cannot be empty.")
|
|
return
|
|
}
|
|
|
|
// Step 3: Node configuration
|
|
fmt.Println()
|
|
fmt.Print("Node name: ")
|
|
nodeName, _ := reader.ReadString('\n')
|
|
nodeName = strings.TrimSpace(nodeName)
|
|
if nodeName == "" {
|
|
fmt.Println("Node name required.")
|
|
return
|
|
}
|
|
|
|
fmt.Print("Listen port [8081]: ")
|
|
port, _ := reader.ReadString('\n')
|
|
port = strings.TrimSpace(port)
|
|
if port == "" {
|
|
port = "8081"
|
|
}
|
|
address := "0.0.0.0:" + port
|
|
|
|
fmt.Print("Upstream IP [127.0.0.1]: ")
|
|
upstreamIP, _ := reader.ReadString('\n')
|
|
upstreamIP = strings.TrimSpace(upstreamIP)
|
|
if upstreamIP == "" {
|
|
upstreamIP = "127.0.0.1"
|
|
}
|
|
|
|
fmt.Print("Upstream port [80]: ")
|
|
upstreamPort, _ := reader.ReadString('\n')
|
|
upstreamPort = strings.TrimSpace(upstreamPort)
|
|
if upstreamPort == "" {
|
|
upstreamPort = "80"
|
|
}
|
|
|
|
fmt.Print("Labels [group=" + nodeName + "]: ")
|
|
labels, _ := reader.ReadString('\n')
|
|
labels = strings.TrimSpace(labels)
|
|
if labels == "" {
|
|
labels = "group=" + nodeName
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Printf(" Node: %s\n", nodeName)
|
|
fmt.Printf(" Listen: %s\n", address)
|
|
fmt.Printf(" Upstream: %s:%s\n", upstreamIP, upstreamPort)
|
|
fmt.Printf(" Region: %s (%s)\n", s.CloudRegion, s.APIHost)
|
|
fmt.Print("\nProceed with deployment? [Y/n]: ")
|
|
confirm, _ := reader.ReadString('\n')
|
|
confirm = strings.TrimSpace(strings.ToLower(confirm))
|
|
if confirm != "" && confirm != "y" && confirm != "yes" {
|
|
fmt.Println("Cancelled.")
|
|
return
|
|
}
|
|
|
|
baseDir := "/opt/wallarm"
|
|
instanceDir := baseDir + "/" + nodeName
|
|
|
|
portNum, _ := strconv.Atoi(upstreamPort)
|
|
s.Nodes = append(s.Nodes, state.Node{
|
|
Name: nodeName,
|
|
Type: "native",
|
|
Address: address,
|
|
UpstreamIP: upstreamIP,
|
|
UpstreamPort: portNum,
|
|
Status: "deploying",
|
|
CreatedAt: time.Now().Format(time.RFC3339),
|
|
})
|
|
|
|
// Step 4: Deploy
|
|
fmt.Println()
|
|
fmt.Println("Starting deployment...")
|
|
|
|
// Create instance directory and .env file
|
|
os.MkdirAll(instanceDir+"/etc", 0755)
|
|
os.MkdirAll(instanceDir+"/var/log", 0755)
|
|
os.MkdirAll(instanceDir+"/var/run", 0755)
|
|
|
|
envContent := fmt.Sprintf(`WALLARM_API_TOKEN=%s
|
|
WALLARM_API_HOST=%s
|
|
WALLARM_LABELS=%s
|
|
WALLARM_LISTEN=%s
|
|
WALLARM_UPSTREAM=%s:%s
|
|
WALLARM_REGION=%s
|
|
`, s.APIToken, s.APIHost, labels, address, upstreamIP, upstreamPort, s.CloudRegion)
|
|
os.WriteFile(instanceDir+"/.env", []byte(envContent), 0600)
|
|
|
|
if err := native.CreateNodesDir(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
return
|
|
}
|
|
if err := native.GenerateSystemdTemplate(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
for _, node := range s.Nodes {
|
|
if err := native.InstallNode(node, s.APIToken, s.APIHost, labels); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Deployment failed: %v\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Save state
|
|
if err := state.Save(&s); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: could not save state: %v\n", err)
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("✅ Deployment complete!")
|
|
fmt.Println()
|
|
fmt.Printf(" Binary: /opt/wallarm/wallarm\n")
|
|
fmt.Printf(" Instance: %s\n", instanceDir)
|
|
fmt.Printf(" State: /opt/wallarm/state.json\n")
|
|
fmt.Println()
|
|
fmt.Println("Run 'sudo /opt/wallarm/wallarm' for the dashboard.")
|
|
}
|
|
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
|
|
|
|
Usage:
|
|
wallarm Start interactive deployment wizard/dashboard
|
|
wallarm --tunnel Start reverse SSH tunnel to sechpoint.app
|
|
wallarm --help Show this help
|
|
|
|
On first run, wallarm checks system readiness, then guides you through
|
|
deployment. On subsequent runs, it shows your existing deployments.
|
|
`)
|
|
}
|
|
help := flag.Bool("help", false, "Show help")
|
|
versionFlag := flag.Bool("version", false, "Show version")
|
|
tunnelFlag := flag.Bool("tunnel", false, "Start reverse SSH tunnel")
|
|
flag.Parse()
|
|
|
|
if *help {
|
|
flag.Usage()
|
|
return
|
|
}
|
|
if *versionFlag {
|
|
fmt.Println("wallarm version", version)
|
|
return
|
|
}
|
|
|
|
// ── Tunnel mode ──────────────────────────────────────────────
|
|
if *tunnelFlag {
|
|
cfg := tunnel.DefaultConfig()
|
|
if tunnelKey != "" {
|
|
cfg.KeyBytes = []byte(tunnelKey)
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "No tunnel key configured. Set WALLARM_TUNNEL_KEY or build with -ldflags.")
|
|
os.Exit(1)
|
|
}
|
|
if err := tunnel.Start(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Tunnel error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
// ── Default: preflight → TUI wizard/dashboard ────────────────
|
|
fmt.Println("═══ Wallarm Deployment Manager ═══")
|
|
fmt.Println()
|
|
|
|
// 1. Preflight checks (always run on start)
|
|
fmt.Println("Running preflight checks...")
|
|
result := preflight.Run()
|
|
for _, c := range result.Checks {
|
|
marker := "✅"
|
|
if !c.Passed {
|
|
marker = "❌"
|
|
} else if c.Warning {
|
|
marker = "⚠️"
|
|
}
|
|
fmt.Printf(" %s %s — %s\n", marker, c.Name, c.Detail)
|
|
}
|
|
|
|
if !result.Passed {
|
|
fmt.Println("\n❌ Preflight checks failed. Fix the issues above and re-run.")
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("✅ Preflight checks passed.")
|
|
fmt.Println()
|
|
|
|
// 2. Launch bubbletea TUI (deploy choice or dashboard)
|
|
if err := ui.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// 3. Post-TUI: if no deployment exists, run deploy form in plain terminal
|
|
if !state.HasDeployment() {
|
|
fmt.Println()
|
|
fmt.Print("Start deployment now? [Y/n]: ")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
answer, _ := reader.ReadString('\n')
|
|
answer = strings.TrimSpace(strings.ToLower(answer))
|
|
if answer == "" || answer == "y" || answer == "yes" {
|
|
runDeployFlow(result)
|
|
}
|
|
}
|
|
}
|