diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..946d1ec --- /dev/null +++ b/Makefile @@ -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/... diff --git a/internal/docker/docker.go b/internal/docker/docker.go new file mode 100644 index 0000000..fbe2e39 --- /dev/null +++ b/internal/docker/docker.go @@ -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, "" +} diff --git a/internal/native/native.go b/internal/native/native.go new file mode 100644 index 0000000..c344c94 --- /dev/null +++ b/internal/native/native.go @@ -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) +}