wallarm/internal/native/native.go

256 lines
7.4 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"
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.
func installNginx() {
if _, err := exec.LookPath("nginx"); err == nil {
return
}
fmt.Println("Installing NGINX (required by Wallarm node)...")
exec.Command("apt-get", "update", "-qq").Run()
exec.Command("apt-get", "install", "-y", "-qq", "nginx").Run()
}
// 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"
}