//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 }