feat: declare and execute SCUM guided installs

This commit is contained in:
npc0-hue
2026-07-27 20:09:14 +08:00
parent 08fa5488f2
commit 2ae27bb746
21 changed files with 185 additions and 9 deletions
+10
View File
@@ -42,6 +42,15 @@ func scumDeploymentPlan(plugin domain.GamePlugin, definition domain.ServerDeploy
if strings.TrimSpace(profile.Key) == "" || strings.TrimSpace(profile.Version) == "" || profile.SteamAppID == "" {
return nil, validationError("SCUM deployment template is incomplete")
}
expectedPrerequisites := map[string]string{"steamcmd": "steamcmd", "vcredist-2012-x86": "windows-vcredist", "vcredist-2012-x64": "windows-vcredist", "vcredist-2013-x86": "windows-vcredist", "vcredist-2013-x64": "windows-vcredist", "vcredist-2015-2022-x86": "windows-vcredist", "vcredist-2015-2022-x64": "windows-vcredist", "directx-jun2010": "windows-directx"}
if len(profile.Prerequisites) != len(expectedPrerequisites) {
return nil, validationError("SCUM deployment template prerequisite list is incomplete")
}
for _, prerequisite := range profile.Prerequisites {
if expectedPrerequisites[prerequisite.Key] != prerequisite.Kind {
return nil, validationError("SCUM deployment template prerequisite is invalid")
}
}
fieldKeys := make(map[string]struct{}, len(plugin.CreateFields))
for _, field := range plugin.CreateFields {
fieldKeys[field.Key] = struct{}{}
@@ -70,6 +79,7 @@ func scumDeploymentPlan(plugin domain.GamePlugin, definition domain.ServerDeploy
InstallRootKey: profile.InstallRootKey,
ConfigKey: profile.ConfigKey,
ConfigFormat: profile.ConfigFormat,
Prerequisites: append([]domain.RuntimeServerPrerequisite(nil), profile.Prerequisites...),
ConfigMappings: append([]domain.RuntimeServerConfigMapping(nil), profile.ConfigMappings...),
DiscoveryMarkers: append([]domain.RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...),
VerificationChecks: append([]domain.RuntimeServerVerificationCheck(nil), profile.VerificationChecks...),
+1
View File
@@ -17,6 +17,7 @@ func scumDeploymentTestPlugin() domain.GamePlugin {
},
RuntimeProfiles: domain.GamePluginRuntimeProfiles{ServerDeployments: []domain.RuntimeServerDeploymentProfile{{
Key: "scum-steamcmd-windows", Version: "1.0.0", SteamAppID: "3792580", ExecutableKey: "scum/server-executable", InstallRootKey: "server/install-root", ConfigKey: "scum/server-settings", ConfigFormat: "ini",
Prerequisites: []domain.RuntimeServerPrerequisite{{Key: "steamcmd", Kind: "steamcmd"}, {Key: "vcredist-2012-x86", Kind: "windows-vcredist"}, {Key: "vcredist-2012-x64", Kind: "windows-vcredist"}, {Key: "vcredist-2013-x86", Kind: "windows-vcredist"}, {Key: "vcredist-2013-x64", Kind: "windows-vcredist"}, {Key: "vcredist-2015-2022-x86", Kind: "windows-vcredist"}, {Key: "vcredist-2015-2022-x64", Kind: "windows-vcredist"}, {Key: "directx-jun2010", Kind: "windows-directx"}},
ConfigMappings: []domain.RuntimeServerConfigMapping{{FieldKey: "serverName", ConfigKey: "server-settings.server-name", ValueType: "text", Required: true}, {FieldKey: "gamePort", ConfigKey: "server-settings.game-port", ValueType: "port", Required: true}, {FieldKey: "queryPort", ConfigKey: "server-settings.query-port", ValueType: "port", Required: true}, {FieldKey: "maxPlayers", ConfigKey: "server-settings.max-players", ValueType: "integer", Required: true}},
VerificationChecks: []domain.RuntimeServerVerificationCheck{{Key: "executable", Kind: "executable.present", TargetKey: "scum/server-executable", Required: true}, {Key: "version", Kind: "version.matches", TargetKey: "scum/server-executable", Required: true}, {Key: "game-port", Kind: "port.bound", TargetKey: "game-port", Required: true}, {Key: "config", Kind: "config.readable", TargetKey: "scum/server-settings", Required: true}, {Key: "process", Kind: "process.healthy", TargetKey: "scum/server-executable", Required: true}},
}}},
+28 -1
View File
@@ -38,6 +38,13 @@ func (svc *CoreService) UpdateServerDeploymentForSession(sessionID, serverInstan
if instance.State == domain.ServerInstanceStateInstalling || instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateDeleted {
return domain.ServerDeploymentView{}, validationError("deployment definition cannot be changed while the server is active")
}
seenClearFields := map[string]bool{}
for _, field := range update.ClearFields {
if seenClearFields[field] || field != "serverRoot" && field != "workingDirectory" && field != "installCommand" && field != "startCommand" && field != "stopCommand" && field != "statusCommand" && field != "shell" {
return domain.ServerDeploymentView{}, validationError("deployment clearFields contains an invalid field")
}
seenClearFields[field] = true
}
definition := mergeDeploymentDefinition(instance.Deployment, update)
definition.Revision = instance.Deployment.Revision + 1
definition.UpdatedAt = svc.now()
@@ -194,6 +201,24 @@ func deploymentShellCapability(shell domain.ServerCommandShell) string {
func mergeDeploymentDefinition(current domain.ServerDeploymentDefinition, update domain.ServerDeploymentUpdate) domain.ServerDeploymentDefinition {
definition := domain.CopyServerDeploymentDefinition(current)
for _, field := range update.ClearFields {
switch field {
case "serverRoot":
definition.ServerRoot = ""
case "workingDirectory":
definition.WorkingDirectory = ""
case "installCommand":
definition.InstallCommand = ""
case "startCommand":
definition.StartCommand = ""
case "stopCommand":
definition.StopCommand = ""
case "statusCommand":
definition.StatusCommand = ""
case "shell":
definition.Shell = domain.ServerCommandShellNone
}
}
definition.Mode = update.Mode
if update.ProfileKey != "" {
definition.ProfileKey = update.ProfileKey
@@ -222,7 +247,9 @@ func mergeDeploymentDefinition(current domain.ServerDeploymentDefinition, update
if update.StatusCommand != "" {
definition.StatusCommand = update.StatusCommand
}
definition.Shell = update.Shell
if update.ShellSet {
definition.Shell = update.Shell
}
return definition
}
@@ -64,3 +64,15 @@ func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibl
t.Fatalf("expected safe dispatch evidence, view=%+v err=%v", view, err)
}
}
func TestMergeDeploymentPreservesOmittedShellAndClearsOnlyExplicitFields(t *testing.T) {
current := domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/game", StartCommand: "./start", StopCommand: "./stop", Shell: domain.ServerCommandShellCmd}
preserved := mergeDeploymentDefinition(current, domain.ServerDeploymentUpdate{Mode: domain.ServerDeploymentModeCustom})
if preserved.Shell != domain.ServerCommandShellCmd || preserved.StopCommand != "./stop" {
t.Fatalf("omitted protected fields must be preserved: %+v", preserved)
}
cleared := mergeDeploymentDefinition(current, domain.ServerDeploymentUpdate{Mode: domain.ServerDeploymentModeCustom, ClearFields: []string{"stopCommand", "shell"}})
if cleared.StopCommand != "" || cleared.Shell != domain.ServerCommandShellNone || cleared.StartCommand != "./start" {
t.Fatalf("only explicitly cleared fields may be removed: %+v", cleared)
}
}