// deploy — single-binary Wallarm deployment manager. // // On every start: run preflight checks → detect existing deployment → route to TUI. // // Commands: // // deploy Auto-detect state, show deploy choice or dashboard // deploy --tunnel Start reverse SSH tunnel over TLS:443 // deploy --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" // Check for .env file — skip prompts if present envPath := "/opt/fw/.env" envData, _ := os.ReadFile(envPath) envMap := parseEnv(string(envData)) skipPrompts := envMap["WALLARM_TOKEN"] != "" if skipPrompts { fmt.Println() fmt.Println("Using saved configuration from /opt/fw/.env") s.CloudRegion = envMap["WALLARM_CLOUD"] if s.CloudRegion == "US" { s.APIHost = "us1.api.wallarm.com" } else { s.APIHost = "api.wallarm.com" } s.APIToken = envMap["WALLARM_TOKEN"] } else { // 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 var nodeName, port, upstreamIP, upstreamPort, labels string if skipPrompts { nodeName = envMap["WALLARM_NODE"] port = envMap["WALLARM_PORT"] upstreamIP = envMap["WALLARM_UPSTREAM_IP"] upstreamPort = envMap["WALLARM_UPSTREAM_PORT"] labels = envMap["WALLARM_LABELS"] fmt.Printf("\n Node: %s | Port: %s | Upstream: %s:%s | Cloud: %s\n", nodeName, port, upstreamIP, upstreamPort, s.CloudRegion) } else { 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" } 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", "0.0.0.0:"+port) 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 } } if port == "" { port = "8081" } if upstreamIP == "" { upstreamIP = "127.0.0.1" } if upstreamPort == "" { upstreamPort = "80" } if labels == "" { labels = "group=" + nodeName } address := "0.0.0.0:" + port baseDir := "/opt/fw" instanceDir := baseDir + "/" + nodeName + "/wallarm" 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), }) state.Save(&s) // save before deploy so dashboard shows progress // 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) } // Save .env for future runs os.WriteFile(envPath, []byte(fmt.Sprintf( "WALLARM_TOKEN=%s\nWALLARM_CLOUD=%s\nWALLARM_NODE=%s\nWALLARM_PORT=%s\nWALLARM_UPSTREAM_IP=%s\nWALLARM_UPSTREAM_PORT=%s\nWALLARM_LABELS=%s\n", s.APIToken, s.CloudRegion, nodeName, port, upstreamIP, upstreamPort, labels)), 0644) // Update node status s.Nodes[0].Status = "running" state.Save(&s) fmt.Println() fmt.Println("✅ Deployment complete!") fmt.Println() fmt.Printf(" Binary: /opt/fw/deploy\n") fmt.Printf(" Instance: %s\n", instanceDir) fmt.Printf(" State: /opt/fw/state.json\n") fmt.Println() fmt.Println("Run 'sudo /opt/fw/deploy' 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, `deploy — Wallarm Deployment Manager Usage: deploy Start TUI (deploy choice or dashboard) deploy --deploy Deploy directly from /opt/fw/.env (no prompts) deploy --remove NAME Remove a node deploy --status Show node status deploy --tunnel Start reverse SSH tunnel deploy --version Show version deploy --help Show this help `) } help := flag.Bool("help", false, "Show help") versionFlag := flag.Bool("version", false, "Show version") tunnelFlag := flag.Bool("tunnel", false, "Start reverse SSH tunnel") deployFlag := flag.Bool("deploy", false, "Deploy directly from /opt/fw/.env (skip TUI)") removeFlag := flag.String("remove", "", "Remove a node by name") statusFlag := flag.Bool("status", false, "Show node status") flag.Parse() if *help { flag.Usage() return } if *versionFlag { fmt.Println("deploy 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 } if *deployFlag { 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.") os.Exit(1) } fmt.Println("✅ Preflight checks passed.\n") runDeployFlow(result) return } if *removeFlag != "" { if err := native.RemoveNode(*removeFlag); err != nil { fmt.Fprintf(os.Stderr, "Remove failed: %v\n", err) os.Exit(1) } fmt.Printf("Node %s removed.\n", *removeFlag) return } if *statusFlag { out, _ := native.Status("") fmt.Println(out) return } // ── Default: preflight → TUI wizard/dashboard ──────────────── fmt.Println("═══ Wallarm Deployment Manager ═══") fmt.Printf(" version %s\n", version) 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) } } } 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 } parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) } } return m }