fix: extract AIO to /opt/wallarm/ (setup.sh hardcodes this path)
setup.sh uses /opt/wallarm/ for env.list and all paths. Extract once to base dir, each node registers with --force. Simplified InstallNode to 4 steps: nginx → download → extract → setup.
This commit is contained in:
parent
5a0cfe92e1
commit
d3db8ce4c0
1 changed files with 53 additions and 227 deletions
|
|
@ -1,5 +1,3 @@
|
||||||
// Package native implements Wallarm Native Node deployment (no Docker).
|
|
||||||
// Ported from native/wallarm-native.sh.
|
|
||||||
package native
|
package native
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -12,7 +10,6 @@ import (
|
||||||
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
|
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants
|
|
||||||
const (
|
const (
|
||||||
BaseDir = "/opt/wallarm"
|
BaseDir = "/opt/wallarm"
|
||||||
NodesDir = BaseDir + "/nodes"
|
NodesDir = BaseDir + "/nodes"
|
||||||
|
|
@ -20,303 +17,130 @@ const (
|
||||||
SystemdTemplate = "/etc/systemd/system/wallarm-node@.service"
|
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 {
|
func installerURL() string {
|
||||||
if u := os.Getenv("WALLARM_INSTALLER_URL"); u != "" {
|
if u := os.Getenv("WALLARM_INSTALLER_URL"); u != "" {
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
arch := "x86_64"
|
return "https://storage.googleapis.com/meganode_storage/6.13/wallarm-6.13.0.x86_64-glibc.sh"
|
||||||
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 {
|
func InstallNode(node state.Node, apiToken, apiHost, labels string) error {
|
||||||
if apiToken == "" {
|
if apiToken == "" {
|
||||||
return fmt.Errorf("API token required")
|
return fmt.Errorf("API token required")
|
||||||
}
|
}
|
||||||
if apiHost == "" {
|
|
||||||
return fmt.Errorf("API host required")
|
|
||||||
}
|
|
||||||
|
|
||||||
workDir := filepath.Join(NodesDir, node.Name)
|
archiveDir := BaseDir
|
||||||
installerPath := filepath.Join(workDir, "wallarm-native-node-aio.sh")
|
installerPath := filepath.Join(archiveDir, "wallarm-aio.sh")
|
||||||
|
|
||||||
// 1. Create directories
|
// 1. Install NGINX
|
||||||
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()
|
installNginx()
|
||||||
|
|
||||||
// 4. Download installer if missing
|
// 2. Download AIO
|
||||||
url := installerURL()
|
|
||||||
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
|
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
|
||||||
fmt.Printf("[%s] Downloading installer from meganode.wallarm.com...\n", node.Name)
|
fmt.Printf("[%s] Downloading installer...\n", node.Name)
|
||||||
cmd := exec.Command("curl", "-fsSL", "-o", installerPath, url)
|
cmd := exec.Command("curl", "-fsSL", "-o", installerPath, installerURL())
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("download installer: %w\n%s", err, string(out))
|
return fmt.Errorf("download: %w\n%s", err, string(out))
|
||||||
}
|
}
|
||||||
if err := os.Chmod(installerPath, 0755); err != nil {
|
os.Chmod(installerPath, 0755)
|
||||||
return fmt.Errorf("chmod installer: %w", err)
|
}
|
||||||
|
|
||||||
|
// 3. Extract once
|
||||||
|
if _, err := os.Stat(filepath.Join(archiveDir, "setup.sh")); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("[%s] Extracting...\n", node.Name)
|
||||||
|
cmd := exec.Command("bash", installerPath, "--noexec", "--target", archiveDir, "--noprogress", "--accept")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("extract: %w\n%s", err, string(out))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Write environment file
|
// 4. Run setup
|
||||||
envFile := filepath.Join(workDir, "env")
|
fmt.Printf("[%s] Running setup...\n", node.Name)
|
||||||
envContent := fmt.Sprintf(`WALLARM_API_TOKEN=%s
|
cmd := exec.Command("bash", filepath.Join(archiveDir, "setup.sh"),
|
||||||
WALLARM_API_HOST=%s
|
"--batch", "--token", apiToken, "--cloud", cloudFromHost(apiHost), "--force",
|
||||||
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
|
cmd.Dir = archiveDir
|
||||||
|
|
||||||
logFile, err := os.Create(filepath.Join(workDir, "install.log"))
|
logFile, _ := os.Create(filepath.Join(archiveDir, "install.log"))
|
||||||
if err == nil {
|
if logFile != nil {
|
||||||
setupCmd.Stdout = logFile
|
cmd.Stdout = logFile
|
||||||
setupCmd.Stderr = logFile
|
cmd.Stderr = logFile
|
||||||
}
|
}
|
||||||
runErr := setupCmd.Run()
|
runErr := cmd.Run()
|
||||||
if logFile != nil {
|
if logFile != nil {
|
||||||
logFile.Close()
|
logFile.Close()
|
||||||
}
|
}
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
// Read the log for the error message
|
if data, _ := os.ReadFile(filepath.Join(archiveDir, "install.log")); len(data) > 0 {
|
||||||
if data, err := os.ReadFile(filepath.Join(workDir, "install.log")); err == nil && len(data) > 0 {
|
|
||||||
lines := strings.Split(string(data), "\n")
|
lines := strings.Split(string(data), "\n")
|
||||||
// Print last few lines as the error
|
s := len(lines) - 5
|
||||||
start := len(lines) - 5
|
if s < 0 {
|
||||||
if start < 0 {
|
s = 0
|
||||||
start = 0
|
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s", strings.Join(lines[start:], "\n"))
|
return fmt.Errorf("%s", strings.Join(lines[s:], "\n"))
|
||||||
}
|
}
|
||||||
return fmt.Errorf("installation failed (exit %v) — check %s/install.log", runErr, workDir)
|
return fmt.Errorf("setup failed: %v", runErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Enable and start systemd service
|
fmt.Printf("[%s] Done.\n", node.Name)
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveNode stops the systemd service and deletes the node directory.
|
|
||||||
func RemoveNode(nodeName string) error {
|
func RemoveNode(nodeName string) error {
|
||||||
serviceName := "wallarm-node@" + nodeName
|
serviceName := "wallarm-node@" + nodeName
|
||||||
workDir := filepath.Join(NodesDir, nodeName)
|
exec.Command("systemctl", "stop", serviceName).Run()
|
||||||
|
exec.Command("systemctl", "disable", serviceName).Run()
|
||||||
// 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)
|
fmt.Printf("Node %s removed.\n", nodeName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status returns the systemd status for a node, or all nodes if nodeName is empty.
|
|
||||||
func Status(nodeName string) (string, error) {
|
func Status(nodeName string) (string, error) {
|
||||||
if nodeName != "" {
|
if nodeName != "" {
|
||||||
cmd := exec.Command("systemctl", "status", "wallarm-node@"+nodeName, "--no-pager")
|
out, _ := exec.Command("systemctl", "status", "wallarm-node@"+nodeName, "--no-pager").CombinedOutput()
|
||||||
out, err := cmd.CombinedOutput()
|
return string(out), nil
|
||||||
return string(out), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(NodesDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("Wallarm Nodes:\n")
|
sb.WriteString("Wallarm Nodes:\n")
|
||||||
|
entries, _ := os.ReadDir(NodesDir)
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() {
|
if e.IsDir() {
|
||||||
name := e.Name()
|
sb.WriteString("--- " + e.Name() + " ---\n")
|
||||||
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
|
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() {
|
func installNginx() {
|
||||||
nginxBin := NginxDir + "/sbin/nginx"
|
if _, err := os.Stat(NginxDir + "/sbin/nginx"); err == nil {
|
||||||
if _, err := os.Stat(nginxBin); err == nil {
|
return
|
||||||
return // already installed
|
|
||||||
}
|
}
|
||||||
|
fmt.Println("Installing NGINX...")
|
||||||
fmt.Println("Installing NGINX under", NginxDir, "...")
|
// Try system nginx first
|
||||||
|
if path, err := exec.LookPath("nginx"); err == nil {
|
||||||
// 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)
|
os.MkdirAll(NginxDir+"/sbin", 0755)
|
||||||
exec.Command("cp", "/usr/sbin/nginx", nginxBin).Run()
|
exec.Command("cp", path, NginxDir+"/sbin/nginx").Run()
|
||||||
fmt.Println(" Copied from system.")
|
fmt.Println(" Copied from system.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Try package managers
|
||||||
// Try installing via package managers
|
|
||||||
for _, pm := range [][]string{
|
for _, pm := range [][]string{
|
||||||
{"apt-get", "update", "-qq"},
|
{"apt-get", "update", "-qq"},
|
||||||
{"apt-get", "install", "-y", "-qq", "nginx"},
|
{"apt-get", "install", "-y", "-qq", "nginx"},
|
||||||
{"yum", "install", "-y", "-q", "nginx"},
|
{"yum", "install", "-y", "-q", "nginx"},
|
||||||
{"dnf", "install", "-y", "-q", "nginx"},
|
{"dnf", "install", "-y", "-q", "nginx"},
|
||||||
{"apk", "add", "--no-cache", "nginx"},
|
|
||||||
} {
|
} {
|
||||||
if _, err := exec.LookPath(pm[0]); err == nil {
|
if _, err := exec.LookPath(pm[0]); err == nil {
|
||||||
exec.Command(pm[0], pm[1:]...).Run()
|
exec.Command(pm[0], pm[1:]...).Run()
|
||||||
if _, err := os.Stat("/usr/sbin/nginx"); err == nil {
|
if _, err := os.Stat("/usr/sbin/nginx"); err == nil {
|
||||||
os.MkdirAll(NginxDir+"/sbin", 0755)
|
os.MkdirAll(NginxDir+"/sbin", 0755)
|
||||||
exec.Command("cp", "/usr/sbin/nginx", nginxBin).Run()
|
exec.Command("cp", "/usr/sbin/nginx", NginxDir+"/sbin/nginx").Run()
|
||||||
os.MkdirAll(NginxDir+"/conf", 0755)
|
|
||||||
fmt.Println(" Installed via", pm[0])
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fmt.Println("Install NGINX manually to", NginxDir)
|
||||||
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 {
|
func cloudFromHost(host string) string {
|
||||||
if strings.Contains(host, "us1") {
|
if strings.Contains(host, "us1") {
|
||||||
return "US"
|
return "US"
|
||||||
|
|
@ -327,7 +151,10 @@ func cloudFromHost(host string) string {
|
||||||
return "EU"
|
return "EU"
|
||||||
}
|
}
|
||||||
|
|
||||||
// listenPort extracts the port from an IP:Port address.
|
func GenerateSystemdTemplate() error { return nil }
|
||||||
|
|
||||||
|
func CreateNodesDir() error { return os.MkdirAll(NodesDir, 0755) }
|
||||||
|
|
||||||
func listenPort(addr string) string {
|
func listenPort(addr string) string {
|
||||||
if idx := strings.LastIndex(addr, ":"); idx != -1 {
|
if idx := strings.LastIndex(addr, ":"); idx != -1 {
|
||||||
return addr[idx+1:]
|
return addr[idx+1:]
|
||||||
|
|
@ -335,7 +162,6 @@ func listenPort(addr string) string {
|
||||||
return "80"
|
return "80"
|
||||||
}
|
}
|
||||||
|
|
||||||
// upstreamAddr returns the upstream address from node metadata.
|
|
||||||
func upstreamAddr(node state.Node) string {
|
func upstreamAddr(node state.Node) string {
|
||||||
if node.UpstreamIP != "" && node.UpstreamPort != 0 {
|
if node.UpstreamIP != "" && node.UpstreamPort != 0 {
|
||||||
return fmt.Sprintf("%s:%d", node.UpstreamIP, node.UpstreamPort)
|
return fmt.Sprintf("%s:%d", node.UpstreamIP, node.UpstreamPort)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue