Fix SCUM RCON runtime profile resolution
This commit is contained in:
@@ -177,21 +177,10 @@ func (svc *CoreService) resolveSourceRCONDispatch(instance domain.ServerInstance
|
|||||||
if !strings.EqualFold(endpoint.Platform, "windows") || !strings.EqualFold(endpoint.Architecture, "amd64") {
|
if !strings.EqualFold(endpoint.Platform, "windows") || !strings.EqualFold(endpoint.Architecture, "amd64") {
|
||||||
return sourceRCONDispatchResolution{}, validationError("unsupported_extension_platform: SCUM Source RCON requires windows/amd64")
|
return sourceRCONDispatchResolution{}, validationError("unsupported_extension_platform: SCUM Source RCON requires windows/amd64")
|
||||||
}
|
}
|
||||||
binding, err := svc.runtimeBindingForServer(instance.ID)
|
binding, profile, err := svc.sourceRCONRuntimeProfile(instance, plugin, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sourceRCONDispatchResolution{}, err
|
return sourceRCONDispatchResolution{}, err
|
||||||
}
|
}
|
||||||
binding, err = normalizeRuntimeBinding(plugin, binding)
|
|
||||||
if err != nil {
|
|
||||||
return sourceRCONDispatchResolution{}, err
|
|
||||||
}
|
|
||||||
if binding.Status != domain.RuntimeBindingStatusComplete || binding.PluginVersion != plugin.Version {
|
|
||||||
return sourceRCONDispatchResolution{}, validationError("runtime binding is incomplete or stale")
|
|
||||||
}
|
|
||||||
profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey)
|
|
||||||
if !exists || !containsString(profile.Capabilities, domain.JobCapabilityRemoteRunRCONCommand) || !runtimePlatformsContain(profile.Platforms, "windows") {
|
|
||||||
return sourceRCONDispatchResolution{}, validationError("selected runtime profile does not support SCUM RCON")
|
|
||||||
}
|
|
||||||
transport, err := sourceRCONTransport(plugin.RuntimeProfiles, profile)
|
transport, err := sourceRCONTransport(plugin.RuntimeProfiles, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sourceRCONDispatchResolution{}, err
|
return sourceRCONDispatchResolution{}, err
|
||||||
@@ -211,6 +200,56 @@ func (svc *CoreService) resolveSourceRCONDispatch(instance domain.ServerInstance
|
|||||||
return sourceRCONDispatchResolution{plugin: plugin, binding: binding, transport: transport, plan: plan}, nil
|
return sourceRCONDispatchResolution{plugin: plugin, binding: binding, transport: transport, plan: plan}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *CoreService) sourceRCONRuntimeProfile(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) (domain.RuntimeBinding, domain.RuntimeLifecycleProfile, error) {
|
||||||
|
if binding, err := svc.runtimeBindingForServer(instance.ID); err == nil {
|
||||||
|
if normalized, normalizeErr := normalizeRuntimeBinding(plugin, binding); normalizeErr == nil {
|
||||||
|
if profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, normalized.ProfileKey); exists && sourceRCONProfileSupports(plugin.RuntimeProfiles, profile, endpoint) {
|
||||||
|
return normalized, profile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !errors.Is(err, repo.ErrNotFound) {
|
||||||
|
return domain.RuntimeBinding{}, domain.RuntimeLifecycleProfile{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key := strings.TrimSpace(instance.Deployment.ProfileKey); key != "" {
|
||||||
|
if profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, key); exists && sourceRCONProfileSupports(plugin.RuntimeProfiles, profile, endpoint) {
|
||||||
|
return transientSourceRCONBinding(instance, plugin, profile), profile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected domain.RuntimeLifecycleProfile
|
||||||
|
for _, profile := range plugin.RuntimeProfiles.LifecycleProfiles {
|
||||||
|
if !sourceRCONProfileSupports(plugin.RuntimeProfiles, profile, endpoint) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if selected.Key != "" && selected.Key != profile.Key {
|
||||||
|
return domain.RuntimeBinding{}, domain.RuntimeLifecycleProfile{}, validationError("selected runtime profile has multiple SCUM RCON candidates")
|
||||||
|
}
|
||||||
|
selected = profile
|
||||||
|
}
|
||||||
|
if selected.Key == "" {
|
||||||
|
return domain.RuntimeBinding{}, domain.RuntimeLifecycleProfile{}, validationError("selected runtime profile does not support SCUM RCON")
|
||||||
|
}
|
||||||
|
return transientSourceRCONBinding(instance, plugin, selected), selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceRCONProfileSupports(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile, endpoint domain.RunEndpoint) bool {
|
||||||
|
if !containsString(profile.Capabilities, domain.JobCapabilityRemoteRunRCONCommand) || !runtimePlatformsContain(profile.Platforms, "windows") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := sourceRCONTransport(profiles, profile); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := sourceRCONExtension(profiles, profile, endpoint); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func transientSourceRCONBinding(instance domain.ServerInstance, plugin domain.GamePlugin, profile domain.RuntimeLifecycleProfile) domain.RuntimeBinding {
|
||||||
|
return domain.RuntimeBinding{ID: "runtime-binding-" + instance.ID, ServerInstanceID: instance.ID, PluginID: plugin.ID, PluginVersion: plugin.Version, ProfileKey: profile.Key, Mode: profile.Mode, Status: domain.RuntimeBindingStatusComplete}
|
||||||
|
}
|
||||||
|
|
||||||
func sourceRCONTransport(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile) (domain.RuntimeTransportProfile, error) {
|
func sourceRCONTransport(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile) (domain.RuntimeTransportProfile, error) {
|
||||||
return sourceRCONTransportForCapability(profiles, profile, "", domain.JobCapabilityRemoteRunRCONCommand)
|
return sourceRCONTransportForCapability(profiles, profile, "", domain.JobCapabilityRemoteRunRCONCommand)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,65 @@ func TestSourceRCONDispatchCanonicalizesLegacyLocalBinding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSourceRCONDispatchSelectsDeclaredProfileWithoutManualBinding(t *testing.T) {
|
||||||
|
svc, session, _, instance := newSourceRCONFixtureWithRuntimeBinding(t, false)
|
||||||
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plugin.RuntimeProfiles.LifecycleProfiles[0].Key = "run-local"
|
||||||
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||||
|
t.Fatalf("update source RCON profile key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch, err := svc.DispatchSourceRCONCommandForSession(session, domain.SourceRCONCommandRequest{ServerInstanceID: instance.ID, Kind: domain.SourceRCONCommandKindCommand, Command: "#ListPlayers", IdempotencyKey: "rcon-without-binding"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatch RCON without manual binding: %v", err)
|
||||||
|
}
|
||||||
|
job, err := svc.store.Jobs().Get(dispatch.JobID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get RCON job: %v", err)
|
||||||
|
}
|
||||||
|
if job.ExecutionInput.WorkspaceScope != "run-local" || job.TargetKey != "rcon" || job.ExecutionInput.SourceRCON == nil {
|
||||||
|
t.Fatalf("expected declared source RCON profile to drive job, got %+v", job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceRCONDispatchFallsBackFromCustomClientBinding(t *testing.T) {
|
||||||
|
svc, session, _, instance := newSourceRCONFixture(t)
|
||||||
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plugin.RuntimeProfiles.LifecycleProfiles[0].Key = "run-local"
|
||||||
|
plugin.RuntimeProfiles.LifecycleProfiles = append(plugin.RuntimeProfiles.LifecycleProfiles, domain.RuntimeLifecycleProfile{Key: "scum-client", Mode: "custom-client", Capabilities: []string{"client-manager.deploy", "client-manager.control", "logs.read"}, ClientManagerRef: "scum-client-manager", Platforms: []string{"windows"}})
|
||||||
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||||
|
t.Fatalf("update plugin profiles: %v", err)
|
||||||
|
}
|
||||||
|
binding, err := svc.store.RuntimeBindings().Get("runtime-binding-" + instance.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get runtime binding: %v", err)
|
||||||
|
}
|
||||||
|
binding.ProfileKey = "scum-client"
|
||||||
|
binding.Mode = "custom-client"
|
||||||
|
binding.Bindings = map[string]string{"scum-client-manager": "runtime-client-manager"}
|
||||||
|
if err := svc.store.RuntimeBindings().Update(binding); err != nil {
|
||||||
|
t.Fatalf("point binding at custom client profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch, err := svc.DispatchSourceRCONCommandForSession(session, domain.SourceRCONCommandRequest{ServerInstanceID: instance.ID, Kind: domain.SourceRCONCommandKindCommand, Command: "#ListPlayers", IdempotencyKey: "rcon-custom-client-binding"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatch RCON with custom-client binding fallback: %v", err)
|
||||||
|
}
|
||||||
|
job, err := svc.store.Jobs().Get(dispatch.JobID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get RCON job: %v", err)
|
||||||
|
}
|
||||||
|
if job.ExecutionInput.WorkspaceScope != "run-local" || job.TargetKey != "rcon" || job.ExecutionInput.RemoteAdapterKey != "rcon" {
|
||||||
|
t.Fatalf("expected source RCON to use run-local despite custom-client binding, got %+v", job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSourceRCONBrokerExpiresWithoutReplay(t *testing.T) {
|
func TestSourceRCONBrokerExpiresWithoutReplay(t *testing.T) {
|
||||||
stamp := fixedTime
|
stamp := fixedTime
|
||||||
broker := newSourceRCONCommandBroker(func() time.Time { return stamp })
|
broker := newSourceRCONCommandBroker(func() time.Time { return stamp })
|
||||||
@@ -139,6 +198,10 @@ func TestSourceRCONBrokerExpiresWithoutReplay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newSourceRCONFixture(t *testing.T) (*CoreService, string, string, domain.ServerInstance) {
|
func newSourceRCONFixture(t *testing.T) (*CoreService, string, string, domain.ServerInstance) {
|
||||||
|
return newSourceRCONFixtureWithRuntimeBinding(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSourceRCONFixtureWithRuntimeBinding(t *testing.T, createBinding bool) (*CoreService, string, string, domain.ServerInstance) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
svc := newCoreService(repo.NewMemoryStore(), func() time.Time { return fixedTime })
|
svc := newCoreService(repo.NewMemoryStore(), func() time.Time { return fixedTime })
|
||||||
capability := domain.JobCapabilityRemoteRunRCONCommand
|
capability := domain.JobCapabilityRemoteRunRCONCommand
|
||||||
@@ -177,12 +240,14 @@ func newSourceRCONFixture(t *testing.T) (*CoreService, string, string, domain.Se
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create RCON server: %v", err)
|
t.Fatalf("create RCON server: %v", err)
|
||||||
}
|
}
|
||||||
binding, err := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{"rcon": "runtime-rcon"}}, true)
|
if createBinding {
|
||||||
if err != nil {
|
binding, err := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{"rcon": "runtime-rcon"}}, true)
|
||||||
t.Fatalf("create RCON binding: %v", err)
|
if err != nil {
|
||||||
}
|
t.Fatalf("create RCON binding: %v", err)
|
||||||
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
|
}
|
||||||
t.Fatalf("store RCON binding: %v", err)
|
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
|
||||||
|
t.Fatalf("store RCON binding: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
helloRequest := validRunControlHello()
|
helloRequest := validRunControlHello()
|
||||||
helloRequest.CapabilityReport.Capabilities = []string{capability}
|
helloRequest.CapabilityReport.Capabilities = []string{capability}
|
||||||
|
|||||||
Reference in New Issue
Block a user