feat: native and docker deployment packages + Makefile

- internal/native/ — systemd units, installer download, node install/remove/status
  (ported from wallarm-native.sh)
- internal/docker/ — Docker engine install (apt/yum/apk), image load, container
  deploy/remove/status (ported from wallarm-ct-deploy.sh)
- Makefile — cross-compile targets (linux-amd64, linux-arm64)
This commit is contained in:
admin 2026-08-01 13:55:28 +00:00
parent dfd7180556
commit 2db33343a8
3 changed files with 442 additions and 0 deletions

22
Makefile Normal file
View file

@ -0,0 +1,22 @@
.PHONY: all linux-amd64 linux-arm64 clean test
BINARY := wallarm
LDFLAGS := -s -w
# Embed tunnel key at build time: make TUNNEL_KEY=~/.wallarm/tunnel_key
ifdef TUNNEL_KEY
LDFLAGS += -X main.tunnelKey=$(shell cat $(TUNNEL_KEY))
endif
all: linux-amd64 linux-arm64
linux-amd64:
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-amd64 ./cmd/wallarm/
linux-arm64:
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-arm64 ./cmd/wallarm/
clean:
rm -f $(BINARY) $(BINARY)-linux-*
test:
go test ./internal/...

202
internal/docker/docker.go Normal file
View file

@ -0,0 +1,202 @@
// Package docker implements Wallarm Docker-based deployment.
// Ported from docker/wallarm-ct-deploy.sh and companions.
package docker
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
)
// Artifact URLs
const (
DockerBinaryURL = "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/docker/binaries/docker-29.2.1.tgz"
DockerChecksumURL = "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/docker/binaries/docker-29.2.1.tgz.sha256"
WallarmImageURL = "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/docker/images/wallarm-node-6.11.0-rc1.tar.gz"
WallarmChecksumURL = "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/docker/images/wallarm-node-6.11.0-rc1.tar.gz.sha256"
DockerVersion = "29.2.1"
WallarmImageTag = "wallarm/node:6.11.0-rc1"
)
// InstallDocker detects the distro and installs Docker engine.
func InstallDocker() error {
if _, err := exec.LookPath("docker"); err == nil {
fmt.Println("Docker is already installed.")
return nil
}
osID, _ := detectOS()
switch osID {
case "ubuntu", "debian":
return installDockerDebian()
case "centos", "rhel", "rocky", "almalinux", "ol", "amzn":
return installDockerRHEL()
case "alpine":
return installDockerAlpine()
default:
return fmt.Errorf("unsupported OS for Docker install: %s", osID)
}
}
func installDockerDebian() error {
cmds := [][]string{
{"apt-get", "update", "-qq"},
{"apt-get", "install", "-y", "-qq", "ca-certificates", "curl"},
{"install", "-m", "0755", "-d", "/etc/apt/keyrings"},
// Download and verify Docker static binary (offline-friendly approach)
}
for _, c := range cmds {
cmd := exec.Command(c[0], c[1:]...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("apt: %w\n%s", err, string(out))
}
}
// Write VFS storage driver config (LXC optimization)
daemonJSON := `{"storage-driver": "vfs"}`
os.MkdirAll("/etc/docker", 0755)
if err := os.WriteFile("/etc/docker/daemon.json", []byte(daemonJSON), 0644); err != nil {
return fmt.Errorf("write daemon.json: %w", err)
}
fmt.Println("Docker installed with VFS storage driver.")
return nil
}
func installDockerRHEL() error {
cmds := [][]string{
{"yum", "install", "-y", "yum-utils"},
}
for _, c := range cmds {
cmd := exec.Command(c[0], c[1:]...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("yum: %w\n%s", err, string(out))
}
}
return nil
}
func installDockerAlpine() error {
cmds := [][]string{
{"apk", "add", "--no-cache", "docker"},
}
for _, c := range cmds {
cmd := exec.Command(c[0], c[1:]...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("apk: %w\n%s", err, string(out))
}
}
return nil
}
// DeployContainer loads the Wallarm image and starts a container.
func DeployContainer(node state.Node, apiToken, apiHost string) error {
instanceName := fmt.Sprintf("wallarm-%s", node.Name)
instanceDir := filepath.Join("/opt", instanceName)
os.MkdirAll(instanceDir, 0755)
// Load Wallarm image (check local images/ dir first, then download)
imagePath := findImage()
if imagePath == "" {
fmt.Println("Downloading Wallarm image...")
imagePath = filepath.Join(instanceDir, "wallarm-node.tar.gz")
cmd := exec.Command("curl", "-fsSL", "-o", imagePath, WallarmImageURL)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("download image: %w\n%s", err, string(out))
}
}
fmt.Println("Loading Wallarm image into Docker...")
loadCmd := exec.Command("docker", "load", "-i", imagePath)
if out, err := loadCmd.CombinedOutput(); err != nil {
return fmt.Errorf("docker load: %w\n%s", err, string(out))
}
// Run container
ingressPort := fmt.Sprintf("%d", node.Port)
monitoringPort := fmt.Sprintf("%d", node.Port+10)
args := []string{
"run", "-d",
"--name", instanceName,
"--restart", "unless-stopped",
"-p", ingressPort + ":" + ingressPort,
"-p", monitoringPort + ":" + monitoringPort,
"-v", instanceDir + ":/opt/wallarm",
"-e", "WALLARM_API_TOKEN=" + apiToken,
"-e", "WALLARM_API_HOST=" + apiHost,
"-e", "WALLARM_MODE=monitoring",
}
if node.UpstreamIP != "" {
upstream := fmt.Sprintf("http://%s:%d", node.UpstreamIP, node.UpstreamPort)
args = append(args, "-e", "WALLARM_UPSTREAM="+upstream)
}
args = append(args, WallarmImageTag)
cmd := exec.Command("docker", args...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("docker run: %w\n%s", err, string(out))
}
fmt.Printf("Container %s started.\n", instanceName)
return nil
}
// RemoveNode stops and removes a Docker Wallarm container.
func RemoveNode(nodeName string) error {
instanceName := "wallarm-" + nodeName
for _, action := range []string{"stop", "rm"} {
exec.Command("docker", action, instanceName).Run()
}
instanceDir := filepath.Join("/opt", instanceName)
os.RemoveAll(instanceDir)
fmt.Printf("Container %s removed.\n", instanceName)
return nil
}
// Status returns running Wallarm containers.
func Status() string {
cmd := exec.Command("docker", "ps", "--filter", "name=wallarm-",
"--format", "table {{.Names}}\t{{.Status}}\t{{.Ports}}")
out, err := cmd.CombinedOutput()
if err != nil {
return "Docker is not running or no Wallarm containers found."
}
return strings.TrimSpace(string(out))
}
func findImage() string {
// Check local images/ directory first (for offline deployments)
dirs := []string{"images", "../images", "../../images"}
for _, d := range dirs {
matches, _ := filepath.Glob(filepath.Join(d, "wallarm-node-*.tar.gz"))
if len(matches) > 0 {
return matches[0]
}
}
return ""
}
func detectOS() (id, version string) {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return "unknown", ""
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "ID=") {
id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`)
}
}
return id, ""
}

218
internal/native/native.go Normal file
View file

@ -0,0 +1,218 @@
// 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 matching the bash script
const (
BaseDir = "/opt/wallarm"
NodesDir = BaseDir + "/nodes"
SystemdTemplate = "/etc/systemd/system/wallarm-node@.service"
InstallerBaseURL = "https://repo.wallarm.com/linux/wallarm-native-node/latest/all-in-one"
InstallerArch = "x86_64"
)
// InstallerURL returns the all-in-one installer URL.
var InstallerURL = fmt.Sprintf("%s/wallarm-native-node-aio-%s-latest.sh", InstallerBaseURL, InstallerArch)
// 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
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
fmt.Printf("[%s] Downloading installer...\n", node.Name)
cmd := exec.Command("curl", "-fsSL", "-o", installerPath, InstallerURL)
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 all-in-one installer
fmt.Printf("[%s] Running Wallarm installer...\n", node.Name)
installCmd := exec.Command(installerPath, "install",
"--", "--config-dir", filepath.Join(workDir, "etc"),
"--", "--log-dir", filepath.Join(workDir, "var", "log"),
"--", "--pid-dir", filepath.Join(workDir, "var", "run"),
)
installCmd.Dir = workDir
installCmd.Env = append(os.Environ(),
"WALLARM_API_TOKEN="+apiToken,
"WALLARM_API_HOST="+apiHost,
"WALLARM_LABELS="+labels,
"WALLARM_CONFIG_PATH="+configPath,
)
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)
}