// Package native implements Wallarm Native Node deployment (no Docker). // Ported from native/wallarm-native.sh. package native import ( "fmt" "os" "os/exec" "path/filepath" "strings" "git.sechpoint.app/customer-engineering/wallarm/internal/state" ) // Constants const ( BaseDir = "/opt/wallarm" NodesDir = BaseDir + "/nodes" NginxDir = BaseDir + "/nginx" SystemdTemplate = "/etc/systemd/system/wallarm-node@.service" ) // installerURL returns the correct Wallarm AIO installer for the current architecture. // Override with WALLARM_INSTALLER_URL env var to pin a specific version. // Source: https://github.com/wallarm/ingress/blob/main/build/fetch-module.sh func installerURL() string { if u := os.Getenv("WALLARM_INSTALLER_URL"); u != "" { return u } arch := "x86_64" libc := "glibc" // glibc for Debian/Ubuntu/RHEL, musl for Alpine return fmt.Sprintf("https://storage.googleapis.com/meganode_storage/6.13/wallarm-6.13.0.%s-%s.sh", arch, libc) } // InstallNode deploys a single native Wallarm node. // node: node metadata, apiToken: Wallarm API token, apiHost: cloud API host, labels: optional node labels. func InstallNode(node state.Node, apiToken, apiHost, labels string) error { if apiToken == "" { return fmt.Errorf("API token required") } if apiHost == "" { return fmt.Errorf("API host required") } workDir := filepath.Join(NodesDir, node.Name) installerPath := filepath.Join(workDir, "wallarm-native-node-aio.sh") // 1. Create directories for _, d := range []string{ filepath.Join(workDir, "etc"), filepath.Join(workDir, "var", "log"), filepath.Join(workDir, "var", "run"), } { if err := os.MkdirAll(d, 0755); err != nil { return fmt.Errorf("mkdir %s: %w", d, err) } } // 2. Write go-node.yaml config configPath := filepath.Join(workDir, "etc", "go-node.yaml") config := fmt.Sprintf(`mode: connector-server connector: address: "%s" `, node.Address) if err := os.WriteFile(configPath, []byte(config), 0644); err != nil { return fmt.Errorf("write config: %w", err) } // 3. Install NGINX (Wallarm node requires it as proxy layer) installNginx() // 4. Download installer if missing url := installerURL() if _, err := os.Stat(installerPath); os.IsNotExist(err) { fmt.Printf("[%s] Downloading installer from meganode.wallarm.com...\n", node.Name) cmd := exec.Command("curl", "-fsSL", "-o", installerPath, url) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("download installer: %w\n%s", err, string(out)) } if err := os.Chmod(installerPath, 0755); err != nil { return fmt.Errorf("chmod installer: %w", err) } } // 4. Write environment file envFile := filepath.Join(workDir, "env") envContent := fmt.Sprintf(`WALLARM_API_TOKEN=%s WALLARM_API_HOST=%s WALLARM_LABELS=%s WALLARM_CONFIG_PATH=%s `, apiToken, apiHost, labels, configPath) if err := os.WriteFile(envFile, []byte(envContent), 0600); err != nil { return fmt.Errorf("write env: %w", err) } // 5. Extract the Makeself archive (--noexec skips running setup.sh) fmt.Printf("[%s] Extracting installer...\n", node.Name) extractCmd := exec.Command("bash", installerPath, "--noexec", "--target", workDir, "--noprogress", "--accept") extractCmd.Dir = workDir if out, err := extractCmd.CombinedOutput(); err != nil { return fmt.Errorf("extract installer: %w\n%s", err, string(out)) } // 6. Set up per-instance NGINX config and binary installNginx() // ensure shared NGINX binary exists instanceNginx := filepath.Join(workDir, "nginx") os.MkdirAll(instanceNginx+"/conf", 0755) os.MkdirAll(instanceNginx+"/logs", 0755) os.MkdirAll(instanceNginx+"/sbin", 0755) exec.Command("cp", NginxDir+"/sbin/nginx", instanceNginx+"/sbin/nginx").Run() nginxConf := instanceNginx + "/conf/nginx.conf" os.WriteFile(nginxConf, []byte(fmt.Sprintf(` worker_processes auto; pid %s/nginx/nginx.pid; error_log %s/nginx/logs/error.log; events { worker_connections 10240; } http { access_log %s/nginx/logs/access.log; server { listen %s; location / { proxy_pass http://%s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } } `, workDir, workDir, workDir, listenPort(node.Address), upstreamAddr(node))), 0644) // 7. Run setup.sh with custom NGINX build (uses our per-instance nginx) setupScript := filepath.Join(workDir, "setup.sh") if _, err := os.Stat(setupScript); err != nil { return fmt.Errorf("setup.sh not found after extraction: %w", err) } fmt.Printf("[%s] Running setup (batch mode)...\n", node.Name) setupCmd := exec.Command("bash", setupScript, "--batch", "--token", apiToken, "--cloud", cloudFromHost(apiHost), "--custom-ngx-build", ) setupCmd.Dir = workDir logFile, err := os.Create(filepath.Join(workDir, "install.log")) if err == nil { setupCmd.Stdout = logFile setupCmd.Stderr = logFile defer logFile.Close() } if err := setupCmd.Run(); err != nil { return fmt.Errorf("installation failed — check %s/install.log: %w", workDir, err) } // 6. Enable and start systemd service serviceName := "wallarm-node@" + node.Name for _, action := range []string{"enable", "start"} { cmd := exec.Command("systemctl", action, serviceName) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("systemctl %s %s: %w\n%s", action, serviceName, err, string(out)) } } fmt.Printf("[%s] Installation successful. Service: %s\n", node.Name, serviceName) return nil } // RemoveNode stops the systemd service and deletes the node directory. func RemoveNode(nodeName string) error { serviceName := "wallarm-node@" + nodeName workDir := filepath.Join(NodesDir, nodeName) // Stop and disable for _, action := range []string{"stop", "disable"} { exec.Command("systemctl", action, serviceName).Run() // ignore errors } // Remove directory if err := os.RemoveAll(workDir); err != nil { return fmt.Errorf("remove %s: %w", workDir, err) } fmt.Printf("Node %s removed.\n", nodeName) return nil } // Status returns the systemd status for a node, or all nodes if nodeName is empty. func Status(nodeName string) (string, error) { if nodeName != "" { cmd := exec.Command("systemctl", "status", "wallarm-node@"+nodeName, "--no-pager") out, err := cmd.CombinedOutput() return string(out), err } entries, err := os.ReadDir(NodesDir) if err != nil { return "", err } var sb strings.Builder sb.WriteString("Wallarm Nodes:\n") for _, e := range entries { if e.IsDir() { name := e.Name() sb.WriteString(fmt.Sprintf("--- %s ---\n", name)) cmd := exec.Command("systemctl", "status", "wallarm-node@"+name, "--no-pager") out, err := cmd.CombinedOutput() if err != nil { sb.WriteString(string(out)) } else { lines := strings.Split(string(out), "\n") for i, l := range lines { if i >= 5 { break } sb.WriteString(l + "\n") } } sb.WriteString("\n") } } return sb.String(), nil } // GenerateSystemdTemplate creates the wallarm-node@.service template unit. func GenerateSystemdTemplate() error { if _, err := os.Stat(SystemdTemplate); err == nil { return nil } content := fmt.Sprintf(`[Unit] Description=Wallarm Node - %%i After=network.target [Service] Type=forking WorkingDirectory=%s/%%i ExecStartPre=%s/%%i/nginx/sbin/nginx -c %s/%%i/nginx/conf/nginx.conf -t ExecStart=%s/%%i/nginx/sbin/nginx -c %s/%%i/nginx/conf/nginx.conf ExecStartPost=/bin/bash %s/%%i/supervisord.sh start ExecStop=/bin/bash %s/%%i/supervisord.sh stop ExecStopPost=/bin/bash -c '%s/%%i/nginx/sbin/nginx -s quit 2>/dev/null || true' Restart=on-failure RestartSec=5 User=root [Install] WantedBy=multi-user.target `, NodesDir, NodesDir, NodesDir, NodesDir, NodesDir, NodesDir, NodesDir, NodesDir) if err := os.WriteFile(SystemdTemplate, []byte(content), 0644); err != nil { return fmt.Errorf("write systemd template: %w", err) } exec.Command("systemctl", "daemon-reload").Run() return nil } // CreateNodesDir ensures the /opt/wallarm/nodes directory exists. func CreateNodesDir() error { return os.MkdirAll(NodesDir, 0755) } // installNginx ensures a dedicated NGINX is present under /opt/wallarm/nginx/. // Tries apt, yum, dnf, apk — prints clear error if all fail. func installNginx() { nginxBin := NginxDir + "/sbin/nginx" if _, err := os.Stat(nginxBin); err == nil { return // already installed } fmt.Println("Installing NGINX under", NginxDir, "...") // Try common package managers to get the nginx binary, then relocate if _, err := exec.LookPath("nginx"); err == nil { // System has nginx, copy the binary os.MkdirAll(NginxDir+"/sbin", 0755) exec.Command("cp", "/usr/sbin/nginx", nginxBin).Run() fmt.Println(" Copied from system.") return } // Try installing via package managers for _, pm := range [][]string{ {"apt-get", "update", "-qq"}, {"apt-get", "install", "-y", "-qq", "nginx"}, {"yum", "install", "-y", "-q", "nginx"}, {"dnf", "install", "-y", "-q", "nginx"}, {"apk", "add", "--no-cache", "nginx"}, } { if _, err := exec.LookPath(pm[0]); err == nil { exec.Command(pm[0], pm[1:]...).Run() if _, err := os.Stat("/usr/sbin/nginx"); err == nil { os.MkdirAll(NginxDir+"/sbin", 0755) exec.Command("cp", "/usr/sbin/nginx", nginxBin).Run() os.MkdirAll(NginxDir+"/conf", 0755) fmt.Println(" Installed via", pm[0]) return } } } fmt.Println("Could not install NGINX. Install it manually to", NginxDir) } // cloudFromHost maps API host to cloud region code (US/EU/ME). func cloudFromHost(host string) string { if strings.Contains(host, "us1") { return "US" } if strings.Contains(host, "me1") { return "ME" } return "EU" } // listenPort extracts the port from an IP:Port address. func listenPort(addr string) string { if idx := strings.LastIndex(addr, ":"); idx != -1 { return addr[idx+1:] } return "80" } // upstreamAddr returns the upstream address from node metadata. func upstreamAddr(node state.Node) string { if node.UpstreamIP != "" && node.UpstreamPort != 0 { return fmt.Sprintf("%s:%d", node.UpstreamIP, node.UpstreamPort) } return "127.0.0.1:80" }