package main import ( "bufio" "flag" "fmt" "os" "os/exec" "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" ) var tunnelKey string var version = "dev" func main() { ver := flag.Bool("version", false, "Show version") tun := flag.Bool("tunnel", false, "Start reverse SSH tunnel") flag.Parse() if *ver { fmt.Println("deploy version", version) return } if *tun { startTunnel() return } // Run preflight, then interactive menu result := preflight.Run() fmt.Println("═══ Wallarm Deployment Manager ═══") fmt.Printf(" version %s\n\n", version) for _, c := range result.Checks { m := "✅" if !c.Passed { m = "❌" } else if c.Warning { m = "⚠️" } fmt.Printf(" %s %s — %s\n", m, c.Name, c.Detail) } if !result.Passed { fmt.Println("\n❌ Preflight checks failed.") os.Exit(1) } fmt.Println("✅ Preflight checks passed.") fmt.Println() showMenu() } func showMenu() { reader := bufio.NewReader(os.Stdin) for { fmt.Println() fmt.Println("─── Main Menu ───") fmt.Println(" [1] Deploy a node") fmt.Println(" [2] Remove a node") fmt.Println(" [3] Show status") fmt.Println(" [4] Remote tunnel") fmt.Println(" [5] Dashboard (TUI)") fmt.Println(" [q] Quit") fmt.Print("\nChoice: ") c, _ := reader.ReadString('\n') switch strings.TrimSpace(c) { case "1": deployMenu() case "2": removeMenu() case "3": showStatus() case "4": startTunnel() case "5": if err := ui.Run(); err != nil { fmt.Println("TUI error:", err) } case "q", "Q": return } } } // ─── .env parsing with per-node keys ────────────────────────────────── type nodeConfig struct { Name string Token string Cloud string Port string UpstreamIP string UpstreamPort string Labels string } func loadNodes() []nodeConfig { data, _ := os.ReadFile("/opt/fw/.env") env := parseEnv(string(data)) // First try NODES list var names []string if v := env["WALLARM_NODES"]; v != "" { names = strings.Split(v, ",") } else if v := env["WALLARM_NODE"]; v != "" { names = []string{v} // single node legacy } var nodes []nodeConfig for _, n := range names { n = strings.TrimSpace(n) if n == "" { continue } nodes = append(nodes, nodeConfig{ Name: n, Token: env["WALLARM_TOKEN_"+n], Cloud: env["WALLARM_CLOUD_"+n], Port: env["WALLARM_PORT_"+n], UpstreamIP: env["WALLARM_UPSTREAM_IP_"+n], UpstreamPort: env["WALLARM_UPSTREAM_PORT_"+n], Labels: env["WALLARM_LABELS_"+n], }) } if len(nodes) == 0 { // Legacy single-node format if env["WALLARM_TOKEN"] != "" { nodes = append(nodes, nodeConfig{ Name: env["WALLARM_NODE"], Token: env["WALLARM_TOKEN"], Cloud: env["WALLARM_CLOUD"], Port: env["WALLARM_PORT"], UpstreamIP: env["WALLARM_UPSTREAM_IP"], UpstreamPort: env["WALLARM_UPSTREAM_PORT"], Labels: env["WALLARM_LABELS"], }) } } return nodes } func isDeployed(name string) bool { s, _ := state.Load() if s == nil { return false } for _, n := range s.Nodes { if n.Name == name { return true } } return false } // ─── Deploy ─────────────────────────────────────────────────────────── func deployMenu() { nodes := loadNodes() if len(nodes) == 0 { fmt.Println("No nodes configured in /opt/fw/.env") fmt.Println("Create one with: WALLARM_TOKEN=... WALLARM_NODE=... etc") return } s, _ := state.Load() deployed := map[string]bool{} if s != nil { for _, n := range s.Nodes { deployed[n.Name] = true } } fmt.Println("\n─── Deploy Node ───") available := []int{} for i, nc := range nodes { marker := " " if deployed[nc.Name] { marker = "✓ " } fmt.Printf(" %s[%d] %s (port %s)\n", marker, i+1, nc.Name, nc.Port) if !deployed[nc.Name] { available = append(available, i) } } if len(available) == 0 { fmt.Println("All nodes deployed.") return } fmt.Print("\nSelect node number (or Enter for all undeployed): ") reader := bufio.NewReader(os.Stdin) choice, _ := reader.ReadString('\n') choice = strings.TrimSpace(choice) var selected []nodeConfig if choice == "" { for _, i := range available { selected = append(selected, nodes[i]) } } else { idx, err := strconv.Atoi(choice) if err != nil || idx < 1 || idx > len(nodes) { fmt.Println("Invalid choice") return } selected = append(selected, nodes[idx-1]) } for _, nc := range selected { fmt.Printf("\nDeploying %s...\n", nc.Name) deployOne(nc) } } func deployOne(nc nodeConfig) { if nc.Token == "" || nc.Name == "" { fmt.Printf(" Skipped: missing token or name\n") return } if nc.Port == "" { nc.Port = "8081" } if nc.UpstreamIP == "" { nc.UpstreamIP = "127.0.0.1" } if nc.UpstreamPort == "" { nc.UpstreamPort = "80" } if nc.Cloud == "" { nc.Cloud = "EU" } if nc.Labels == "" { nc.Labels = "group=" + nc.Name } apiHost := "api.wallarm.com" if nc.Cloud == "US" { apiHost = "us1.api.wallarm.com" } portNum, _ := strconv.Atoi(nc.UpstreamPort) node := state.Node{ Name: nc.Name, Type: "native", Address: "0.0.0.0:" + nc.Port, UpstreamIP: nc.UpstreamIP, UpstreamPort: portNum, Status: "deploying", CreatedAt: time.Now().Format(time.RFC3339), } // Save state before deploy s, _ := state.Load() if s == nil { s = &state.State{DeploymentType: "native"} } s.Nodes = append(s.Nodes, node) state.Save(s) fmt.Println("Starting deployment...") if err := native.InstallNode(node, nc.Token, apiHost, nc.Labels); err != nil { fmt.Fprintf(os.Stderr, "Deployment failed: %v\n", err) return } // Update status for i := range s.Nodes { if s.Nodes[i].Name == nc.Name { s.Nodes[i].Status = "running" } } state.Save(s) fmt.Printf("✅ %s deployed.\n", nc.Name) } // ─── Remove ─────────────────────────────────────────────────────────── func removeMenu() { s, _ := state.Load() if s == nil || len(s.Nodes) == 0 { fmt.Println("No nodes deployed.") return } fmt.Println("\n─── Remove Node ───") for i, n := range s.Nodes { status := "●" out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput() if strings.TrimSpace(string(out)) != "active" { status = "○" } fmt.Printf(" [%d] %s %s %s\n", i+1, status, n.Name, n.Address) } fmt.Print("\nSelect node number: ") reader := bufio.NewReader(os.Stdin) choice, _ := reader.ReadString('\n') idx, err := strconv.Atoi(strings.TrimSpace(choice)) if err != nil || idx < 1 || idx > len(s.Nodes) { fmt.Println("Invalid choice") return } n := s.Nodes[idx-1] fmt.Printf("Removing %s...\n", n.Name) if err := native.RemoveNode(n.Name); err != nil { fmt.Println("Error:", err) return } // Update state s.Nodes = append(s.Nodes[:idx-1], s.Nodes[idx:]...) state.Save(s) fmt.Printf("✅ %s removed.\n", n.Name) } // ─── Status ─────────────────────────────────────────────────────────── func showStatus() { s, _ := state.Load() if s == nil || len(s.Nodes) == 0 { fmt.Println("No nodes deployed.") return } fmt.Println("\n─── Node Status ───") for _, n := range s.Nodes { out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput() status := strings.TrimSpace(string(out)) marker := "●" if status != "active" { marker = "○" } fmt.Printf(" %s %s — %s — %s\n", marker, n.Name, status, n.Address) } } // ─── Tunnel ─────────────────────────────────────────────────────────── func startTunnel() { reader := bufio.NewReader(os.Stdin) fmt.Print("Jumphost URL [ssh.sechpoint.app:443]: ") h, _ := reader.ReadString('\n') h = strings.TrimSpace(h) if h == "" { h = "ssh.sechpoint.app:443" } fmt.Print("Username [wallarm-tunnel]: ") u, _ := reader.ReadString('\n') u = strings.TrimSpace(u) if u == "" { u = "wallarm-tunnel" } fmt.Print("Password or SSH key path: ") p, _ := reader.ReadString('\n') p = strings.TrimSpace(p) cfg := tunnel.Config{Jumphost: h, User: u, RemotePort: 9042, LocalSSHPort: 22} if strings.HasPrefix(p, "/") { cfg.KeyPath = p } else { cfg.Password = p } fmt.Printf("\nShare: ssh -p 9042 %s@%s\nCtrl+C to close.\n\n", u, h) tunnel.Start(cfg) } // ─── Helpers ────────────────────────────────────────────────────────── func parseEnv(data string) map[string]string { m := map[string]string{} for _, line := range strings.Split(data, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } p := strings.SplitN(line, "=", 2) if len(p) == 2 { m[strings.TrimSpace(p[0])] = strings.TrimSpace(p[1]) } } return m }