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:
parent
dfd7180556
commit
2db33343a8
3 changed files with 442 additions and 0 deletions
22
Makefile
Normal file
22
Makefile
Normal 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
202
internal/docker/docker.go
Normal 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
218
internal/native/native.go
Normal 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)
|
||||
}
|
||||
Loading…
Reference in a new issue