Go deployment orchestrator with platform-specific SteamCMD install, Rust server download, server.cfg generation, and service registration. Wire deploy command subscription in daemon, make GameServerPath optional, add InstallDir config with OS-aware defaults. Fix unused imports and WebSocket subscribe API in ServerView. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
4.3 KiB
Go
128 lines
4.3 KiB
Go
//go:build linux
|
|
|
|
package deploy
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
)
|
|
|
|
// InstallSteamCMD downloads and installs SteamCMD for Linux into the given
|
|
// install directory. If SteamCMD is already present it returns the existing
|
|
// path without re-downloading. The returned string is the absolute path to
|
|
// the steamcmd.sh executable.
|
|
func InstallSteamCMD(installDir string) (string, error) {
|
|
steamcmdDir := filepath.Join(installDir, "steamcmd")
|
|
steamcmdPath := filepath.Join(steamcmdDir, "steamcmd.sh")
|
|
|
|
// Already installed — nothing to do.
|
|
if _, err := os.Stat(steamcmdPath); err == nil {
|
|
log.Printf("SteamCMD already installed at %s", steamcmdPath)
|
|
return steamcmdPath, nil
|
|
}
|
|
|
|
if err := os.MkdirAll(steamcmdDir, 0755); err != nil {
|
|
return "", fmt.Errorf("failed to create steamcmd directory %s: %w", steamcmdDir, err)
|
|
}
|
|
|
|
// Download the Linux tarball.
|
|
tarball := filepath.Join(steamcmdDir, "steamcmd_linux.tar.gz")
|
|
if err := downloadFile("https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz", tarball); err != nil {
|
|
return "", fmt.Errorf("failed to download steamcmd: %w", err)
|
|
}
|
|
|
|
// Extract with tar.
|
|
cmd := exec.Command("tar", "-xzf", "steamcmd_linux.tar.gz")
|
|
cmd.Dir = steamcmdDir
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return "", fmt.Errorf("failed to extract steamcmd: %w — output: %s", err, string(out))
|
|
}
|
|
|
|
// Ensure the script is executable.
|
|
if err := os.Chmod(steamcmdPath, 0755); err != nil {
|
|
return "", fmt.Errorf("failed to chmod steamcmd.sh: %w", err)
|
|
}
|
|
|
|
// Verify the installation by running +quit (triggers first-time setup).
|
|
verify := exec.Command(steamcmdPath, "+quit")
|
|
verify.Dir = steamcmdDir
|
|
if out, err := verify.CombinedOutput(); err != nil {
|
|
return "", fmt.Errorf("steamcmd verification failed: %w — output: %s", err, string(out))
|
|
}
|
|
|
|
log.Printf("SteamCMD installed successfully at %s", steamcmdPath)
|
|
return steamcmdPath, nil
|
|
}
|
|
|
|
// RegisterService creates a systemd unit file for the Rust Dedicated Server
|
|
// and enables it. If the caller does not have root access, the unit file is
|
|
// written into installDir as a fallback so the user can install it manually.
|
|
func RegisterService(installDir string, cfg DeployConfig) error {
|
|
serverPath := filepath.Join(installDir, "server", "RustDedicated")
|
|
|
|
unit := fmt.Sprintf(`[Unit]
|
|
Description=Rust Dedicated Server (Corrosion Managed)
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
WorkingDirectory=%s/server
|
|
ExecStart=%s -batchmode +server.hostname "%s" +server.port %d +rcon.port %d +rcon.password "%s" +rcon.web 1 +server.identity "corrosion" +server.maxplayers %d +server.worldsize %d +server.seed %d
|
|
Restart=on-failure
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`, installDir, serverPath, cfg.ServerName, cfg.ServerPort, cfg.RconPort,
|
|
cfg.RconPassword, cfg.MaxPlayers, cfg.WorldSize, cfg.Seed)
|
|
|
|
systemdPath := "/etc/systemd/system/rustserver.service"
|
|
if err := os.WriteFile(systemdPath, []byte(unit), 0644); err != nil {
|
|
// Fallback — write into installDir so the user can place it manually.
|
|
fallback := filepath.Join(installDir, "rustserver.service")
|
|
log.Printf("WARNING: cannot write to %s (%v), falling back to %s", systemdPath, err, fallback)
|
|
if writeErr := os.WriteFile(fallback, []byte(unit), 0644); writeErr != nil {
|
|
return fmt.Errorf("failed to write service file to fallback %s: %w", fallback, writeErr)
|
|
}
|
|
}
|
|
|
|
// Best-effort daemon-reload and enable — ignore errors (systemctl may not
|
|
// exist or the user may lack privileges).
|
|
_ = exec.Command("systemctl", "daemon-reload").Run()
|
|
_ = exec.Command("systemctl", "enable", "rustserver").Run()
|
|
|
|
log.Println("Systemd service registered for rustserver")
|
|
return nil
|
|
}
|
|
|
|
// downloadFile fetches url and writes the response body to dest on disk.
|
|
func downloadFile(url, dest string) error {
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return fmt.Errorf("GET %s failed: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("GET %s returned status %d", url, resp.StatusCode)
|
|
}
|
|
|
|
out, err := os.Create(dest)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file %s: %w", dest, err)
|
|
}
|
|
defer out.Close()
|
|
|
|
if _, err := io.Copy(out, resp.Body); err != nil {
|
|
return fmt.Errorf("failed to write to %s: %w", dest, err)
|
|
}
|
|
|
|
return nil
|
|
}
|