// 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 native node installer for the current architecture. // Override with WALLARM_INSTALLER_URL env var to pin a specific version. func installerURL() string { if u := os.Getenv("WALLARM_INSTALLER_URL"); u != "" { return u } arch := "x86_64" suffix := "glibc" return fmt.Sprintf("https://meganode.wallarm.com/6.12/wallarm-6.12.7.%s-%s.sh", arch, suffix) } // 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. 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. Run the meganode installer (piped token + region to avoid interactive prompts) fmt.Printf("[%s] Running Wallarm installer...\n", node.Name) installCmd := exec.Command(installerPath) installCmd.Dir = workDir installCmd.Env = append(os.Environ(), "WALLARM_API_TOKEN="+apiToken, "WALLARM_API_HOST="+apiHost, "WALLARM_LABELS="+labels, ) // Pipe token and region as stdin to skip interactive prompts installCmd.Stdin = strings.NewReader(apiToken + "\n" + apiHost + "\n") logFile, err := os.Create(filepath.Join(workDir, "install.log")) if err == nil { installCmd.Stdout = logFile installCmd.Stderr = logFile defer logFile.Close() } if err := installCmd.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) }