289 lines
8.5 KiB
Go
289 lines
8.5 KiB
Go
// 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. Run setup.sh in batch mode
|
|
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 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"
|
|
}
|