// 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" 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. Run setup.sh in batch mode (non-interactive) 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), ) 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 if it doesn't exist. func GenerateSystemdTemplate() error { if _, err := os.Stat(SystemdTemplate); err == nil { return nil // already exists } content := fmt.Sprintf(`[Unit] Description=Wallarm Native Node - %%I After=network.target [Service] Type=simple WorkingDirectory=%s/%%i EnvironmentFile=%s/%%i/env ExecStart=%s/%%i/wallarm-native-node-aio.sh start ExecStop=%s/%%i/wallarm-native-node-aio.sh stop Restart=on-failure RestartSec=5 User=root [Install] WantedBy=multi-user.target `, NodesDir, NodesDir, NodesDir, NodesDir) if err := os.WriteFile(SystemdTemplate, []byte(content), 0644); err != nil { return fmt.Errorf("write systemd template: %w", err) } cmd := exec.Command("systemctl", "daemon-reload") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("daemon-reload: %w\n%s", err, string(out)) } return nil } // CreateNodesDir ensures the /opt/wallarm/nodes directory exists. func CreateNodesDir() error { return os.MkdirAll(NodesDir, 0755) } // installNginx ensures NGINX is present on the system. // Tries apt, yum, dnf, apk — prints clear error if all fail. func installNginx() { if _, err := exec.LookPath("nginx"); err == nil { return } fmt.Println("Installing NGINX (required by Wallarm node)...") // Try common 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 { cmd := exec.Command(pm[0], pm[1:]...) if out, err := cmd.CombinedOutput(); err == nil { fmt.Println(" Installed via", pm[0]) return } else { _ = out } } } // If all fail, print instructions fmt.Println("Could not install NGINX automatically.") fmt.Println("Install it manually: apt-get install nginx") fmt.Println("Then re-run the deployment.") } // 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" }