feat: interactive menu, multi-node .env, numbered remove
- No flags needed — interactive menu on startup - [1] Deploy: shows .env nodes, ✓ marks deployed, pick by number - [2] Remove: numbered list of deployed nodes from state.json - [3] Status: shows all nodes with systemd state - [4] Tunnel, [5] Dashboard (TUI) - .env supports WALLARM_NODES=srv1,srv2 with per-node keys - Backward compatible with legacy single-node .env
This commit is contained in:
parent
86a172a4ed
commit
221b1968b0
1 changed files with 285 additions and 378 deletions
|
|
@ -1,12 +1,3 @@
|
|||
// 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 (
|
||||
|
|
@ -14,6 +5,7 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -25,406 +17,321 @@ import (
|
|||
"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
|
||||
}
|
||||
|
||||
fmt.Printf("Installing %d node(s)...\n", len(s.Nodes))
|
||||
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")
|
||||
ver := flag.Bool("version", false, "Show version")
|
||||
tun := flag.Bool("tunnel", false, "Start reverse SSH tunnel")
|
||||
flag.Parse()
|
||||
|
||||
if *help {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
if *versionFlag {
|
||||
if *ver {
|
||||
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)
|
||||
}
|
||||
if *tun {
|
||||
startTunnel()
|
||||
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...")
|
||||
// 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 {
|
||||
marker := "✅"
|
||||
if !c.Passed {
|
||||
marker = "❌"
|
||||
} else if c.Warning {
|
||||
marker = "⚠️"
|
||||
}
|
||||
fmt.Printf(" %s %s — %s\n", marker, c.Name, c.Detail)
|
||||
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. Fix the issues above and re-run.")
|
||||
fmt.Println("\n❌ Preflight checks failed.")
|
||||
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)
|
||||
}
|
||||
showMenu()
|
||||
}
|
||||
|
||||
// 3. Post-TUI: if no deployment exists, run deploy form in plain terminal
|
||||
if !state.HasDeployment() {
|
||||
func showMenu() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
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)
|
||||
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
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue