wallarm/cmd/deploy/main.go

407 lines
11 KiB
Go

// 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),
})
// 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)
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 --tunnel Start reverse SSH tunnel
deploy --version Show version
deploy --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")
deployFlag := flag.Bool("deploy", false, "Deploy directly from /opt/fw/.env (skip TUI)")
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
}
// ── 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
}