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>
181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
package deploy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// GameServerStarter abstracts the game server process manager so the deployer
|
|
// can set the executable path and start the server without depending on the
|
|
// concrete process.GameServer type. The existing GameServer will implement
|
|
// UpdatePath in a separate task.
|
|
type GameServerStarter interface {
|
|
Start() error
|
|
UpdatePath(path string)
|
|
}
|
|
|
|
// Deployer orchestrates one-click Rust server deployment. It downloads SteamCMD,
|
|
// installs the Rust Dedicated Server, generates server.cfg, and starts the server
|
|
// process — publishing progress updates to NATS at each stage so the frontend can
|
|
// display real-time deployment status.
|
|
type Deployer struct {
|
|
nc *nats.Conn
|
|
licenseID string
|
|
installDir string
|
|
gameServer GameServerStarter
|
|
}
|
|
|
|
// NewDeployer creates a new Deployer instance.
|
|
func NewDeployer(nc *nats.Conn, licenseID, installDir string, gs GameServerStarter) *Deployer {
|
|
return &Deployer{
|
|
nc: nc,
|
|
licenseID: licenseID,
|
|
installDir: installDir,
|
|
gameServer: gs,
|
|
}
|
|
}
|
|
|
|
// Deploy executes the full deployment pipeline: SteamCMD install, Rust server
|
|
// download, config generation, and server startup. If any stage fails, a "failed"
|
|
// status is published and the error is returned. Progress updates are published
|
|
// to NATS at each stage transition.
|
|
func (d *Deployer) Deploy(cfg DeployConfig) error {
|
|
// Stage 1: SteamCMD
|
|
log.Printf("Deploy: starting SteamCMD installation for license %s", d.licenseID)
|
|
d.publishStatus("downloading_steamcmd", 0, "Checking for existing SteamCMD installation...")
|
|
|
|
steamcmdPath, err := InstallSteamCMD(d.installDir)
|
|
if err != nil {
|
|
d.publishStatus("failed", 0, "SteamCMD installation failed", err.Error())
|
|
return fmt.Errorf("steamcmd install failed: %w", err)
|
|
}
|
|
|
|
log.Printf("Deploy: SteamCMD ready at %s", steamcmdPath)
|
|
d.publishStatus("downloading_steamcmd", 100, "SteamCMD ready")
|
|
|
|
// Stage 2: Download Rust Dedicated Server
|
|
log.Printf("Deploy: downloading Rust Dedicated Server via SteamCMD")
|
|
d.publishStatus("downloading_rust", 0, "Downloading Rust Dedicated Server via SteamCMD...")
|
|
|
|
if err := DownloadRustServer(steamcmdPath, d.installDir); err != nil {
|
|
d.publishStatus("failed", 0, "Rust server download failed", err.Error())
|
|
return fmt.Errorf("rust server download failed: %w", err)
|
|
}
|
|
|
|
log.Printf("Deploy: Rust Dedicated Server installed")
|
|
d.publishStatus("downloading_rust", 100, "Rust Dedicated Server installed")
|
|
|
|
// Stage 3: Generate server.cfg
|
|
log.Printf("Deploy: generating server.cfg")
|
|
d.publishStatus("configuring", 0, "Generating server.cfg...")
|
|
|
|
if err := GenerateServerCfg(d.installDir, cfg); err != nil {
|
|
d.publishStatus("failed", 0, "Server configuration failed", err.Error())
|
|
return fmt.Errorf("config generation failed: %w", err)
|
|
}
|
|
|
|
log.Printf("Deploy: server.cfg written")
|
|
d.publishStatus("configuring", 100, "Server configured")
|
|
|
|
// Stage 4: Start the server
|
|
log.Printf("Deploy: starting Rust server")
|
|
d.publishStatus("starting", 0, "Starting Rust server...")
|
|
|
|
var exePath string
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
exePath = filepath.Join(d.installDir, "server", "RustDedicated.exe")
|
|
default:
|
|
exePath = filepath.Join(d.installDir, "server", "RustDedicated")
|
|
}
|
|
|
|
d.gameServer.UpdatePath(exePath)
|
|
|
|
if err := d.gameServer.Start(); err != nil {
|
|
d.publishStatus("failed", 0, "Server failed to start", err.Error())
|
|
return fmt.Errorf("server start failed: %w", err)
|
|
}
|
|
|
|
log.Printf("Deploy: Rust server is now running")
|
|
d.publishStatus("online", 100, "Rust server is now running")
|
|
|
|
return nil
|
|
}
|
|
|
|
// DownloadRustServer runs SteamCMD to download/update the Rust Dedicated Server
|
|
// (App ID 258550) into {installDir}/server. This function is platform-agnostic —
|
|
// it simply executes the steamcmd binary which was installed by the platform-specific
|
|
// InstallSteamCMD function.
|
|
func DownloadRustServer(steamcmdPath, installDir string) error {
|
|
serverDir := filepath.Join(installDir, "server")
|
|
|
|
log.Printf("Downloading Rust Dedicated Server to %s", serverDir)
|
|
|
|
cmd := exec.Command(steamcmdPath,
|
|
"+login", "anonymous",
|
|
"+force_install_dir", serverDir,
|
|
"+app_update", "258550", "validate",
|
|
"+quit",
|
|
)
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("steamcmd app_update 258550 failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckServerInstalled returns true if the Rust Dedicated Server executable
|
|
// exists at the expected path within the install directory.
|
|
func CheckServerInstalled(installDir string) bool {
|
|
var exePath string
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
exePath = filepath.Join(installDir, "server", "RustDedicated.exe")
|
|
default:
|
|
exePath = filepath.Join(installDir, "server", "RustDedicated")
|
|
}
|
|
|
|
_, err := os.Stat(exePath)
|
|
return err == nil
|
|
}
|
|
|
|
// publishStatus publishes a DeployStatus message to the NATS subject
|
|
// corrosion.{licenseID}.deploy.status. Publish errors are logged but do not
|
|
// fail the deployment — losing a progress update is not fatal.
|
|
func (d *Deployer) publishStatus(stage string, progress int, message string, errDetail ...string) {
|
|
subject := fmt.Sprintf("corrosion.%s.deploy.status", d.licenseID)
|
|
|
|
status := DeployStatus{
|
|
Stage: stage,
|
|
Progress: progress,
|
|
Message: message,
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
if len(errDetail) > 0 && errDetail[0] != "" {
|
|
status.Error = errDetail[0]
|
|
}
|
|
|
|
data, err := json.Marshal(status)
|
|
if err != nil {
|
|
log.Printf("Failed to marshal deploy status: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := d.nc.Publish(subject, data); err != nil {
|
|
log.Printf("Failed to publish deploy status to %s: %v", subject, err)
|
|
}
|
|
}
|