Fix SCUM generated run deployment flow
This commit is contained in:
@@ -129,7 +129,7 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
// queueManagedGuidedDeploymentAfterRegistration advances only a newly-created,
|
// queueManagedGuidedDeploymentAfterRegistration advances only a newly-created,
|
||||||
// target-bound guided server. Selecting guided-install is the owner's prior
|
// dedicated guided server. Selecting guided-install is the owner's prior
|
||||||
// authorization for this bounded write; reconnects remain idempotent.
|
// authorization for this bounded write; reconnects remain idempotent.
|
||||||
func (svc *CoreService) queueManagedGuidedDeploymentAfterRegistration(hello domain.RunControlHello) error {
|
func (svc *CoreService) queueManagedGuidedDeploymentAfterRegistration(hello domain.RunControlHello) error {
|
||||||
if hello.ComponentKind != domain.DistributionComponentRun || strings.TrimSpace(hello.ServerInstanceID) == "" {
|
if hello.ComponentKind != domain.DistributionComponentRun || strings.TrimSpace(hello.ServerInstanceID) == "" {
|
||||||
@@ -139,7 +139,7 @@ func (svc *CoreService) queueManagedGuidedDeploymentAfterRegistration(hello doma
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(instance.DeploymentTargetID) == "" || instance.RunEndpointID != hello.RunEndpointID || instance.State != domain.ServerInstanceStateDraft || instance.Deployment.Mode != domain.ServerDeploymentModeGuided {
|
if instance.RunEndpointID != hello.RunEndpointID || instance.RunEndpointID != dedicatedRunEndpointID(instance.ID) || instance.State != domain.ServerInstanceStateDraft || instance.Deployment.Mode != domain.ServerDeploymentModeGuided {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
_, err = svc.deployServerInstance(domain.ServerLifecycleCommand{
|
_, err = svc.deployServerInstance(domain.ServerLifecycleCommand{
|
||||||
@@ -158,7 +158,7 @@ func (svc *CoreService) validateDedicatedRunHello(hello domain.RunControlHello)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(instance.DeploymentTargetID) == "" {
|
if strings.TrimSpace(instance.DeploymentTargetID) == "" && instance.RunEndpointID != dedicatedRunEndpointID(instance.ID) {
|
||||||
return nil // legacy Run registrations keep their historical endpoint contract.
|
return nil // legacy Run registrations keep their historical endpoint contract.
|
||||||
}
|
}
|
||||||
if hello.PluginID != instance.PluginID || hello.RunEndpointID != instance.RunEndpointID {
|
if hello.PluginID != instance.PluginID || hello.RunEndpointID != instance.RunEndpointID {
|
||||||
|
|||||||
@@ -303,6 +303,29 @@ func TestCoreServiceDedicatedRunRegistrationAutomaticallyDeploysGuidedDraftOnly(
|
|||||||
t.Fatalf("Run reconnect must not duplicate automatic install, jobs=%+v", jobs)
|
t.Fatalf("Run reconnect must not duplicate automatic install, jobs=%+v", jobs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generatedRunDraft := domain.ServerInstance{
|
||||||
|
ID: "managed-generated",
|
||||||
|
PluginID: plugin.ID,
|
||||||
|
PluginVersion: plugin.Version,
|
||||||
|
RunEndpointID: dedicatedRunEndpointID("managed-generated"),
|
||||||
|
Name: "Managed Generated",
|
||||||
|
State: domain.ServerInstanceStateDraft,
|
||||||
|
ConfigVersion: 1,
|
||||||
|
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: "C:\\generated-scum", ProfileKey: "local", Revision: 1},
|
||||||
|
}
|
||||||
|
if err := svc.store.ServerInstances().Create(generatedRunDraft); err != nil {
|
||||||
|
t.Fatalf("create generated Run draft: %v", err)
|
||||||
|
}
|
||||||
|
registerDedicatedRunForTest(t, svc, generatedRunDraft, plugin.ID)
|
||||||
|
storedGenerated, err := svc.GetServerInstance(generatedRunDraft.ID)
|
||||||
|
if err != nil || storedGenerated.State != domain.ServerInstanceStateInstalling {
|
||||||
|
t.Fatalf("generated Run registration should queue install without deployment target, server=%+v err=%v", storedGenerated, err)
|
||||||
|
}
|
||||||
|
jobs, err = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: generatedRunDraft.ID})
|
||||||
|
if err != nil || len(jobs) != 1 || jobs[0].Capability != domain.LifecycleCapabilityInstall {
|
||||||
|
t.Fatalf("expected one automatic generated Run install job, jobs=%+v err=%v", jobs, err)
|
||||||
|
}
|
||||||
|
|
||||||
existing, err := svc.CreateServerInstanceWorkflowForSession(owner, domain.ServerLifecycleCreate{ID: "managed-existing", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Managed Existing", IdempotencyKey: "managed-existing-create", ProfileKey: "local", Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeExisting, ServerRoot: "C:\\existing-scum"}})
|
existing, err := svc.CreateServerInstanceWorkflowForSession(owner, domain.ServerLifecycleCreate{ID: "managed-existing", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Managed Existing", IdempotencyKey: "managed-existing-create", ProfileKey: "local", Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeExisting, ServerRoot: "C:\\existing-scum"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create existing draft: %v", err)
|
t.Fatalf("create existing draft: %v", err)
|
||||||
@@ -314,6 +337,114 @@ func TestCoreServiceDedicatedRunRegistrationAutomaticallyDeploysGuidedDraftOnly(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCoreServiceGeneratedSCUMRunRegistrationQueuesGuidedInstall(t *testing.T) {
|
||||||
|
svc := newTestCoreService()
|
||||||
|
plugin := scumDeploymentTestPlugin()
|
||||||
|
plugin.Name = "SCUM"
|
||||||
|
plugin.Version = "0.1.1"
|
||||||
|
plugin.ServerType = "scum"
|
||||||
|
plugin.Status = domain.GamePluginStatusInstalled
|
||||||
|
plugin.SupportedOS = []string{"windows"}
|
||||||
|
plugin.RequiredRunCapabilities = []string{
|
||||||
|
domain.LifecycleCapabilityInstall,
|
||||||
|
domain.LifecycleCapabilityStart,
|
||||||
|
domain.LifecycleCapabilityStop,
|
||||||
|
"process.restart",
|
||||||
|
domain.LifecycleCapabilityStatus,
|
||||||
|
domain.JobCapabilitySCUMDeploymentPlan,
|
||||||
|
"files.list",
|
||||||
|
domain.JobCapabilityFilesRead,
|
||||||
|
"files.patch",
|
||||||
|
"logs.read",
|
||||||
|
domain.JobCapabilityClientManagerDeploy,
|
||||||
|
domain.JobCapabilityClientManagerControl,
|
||||||
|
domain.JobCapabilityClientManagerUpdate,
|
||||||
|
domain.JobCapabilityClientManagerRollback,
|
||||||
|
domain.JobCapabilityClientManagerUninstall,
|
||||||
|
"artifacts.read",
|
||||||
|
"artifacts.write",
|
||||||
|
"ai.invoke",
|
||||||
|
}
|
||||||
|
plugin.DeclaredPermissions = []string{"server.run.distribution"}
|
||||||
|
plugin.LifecycleActions = domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"}
|
||||||
|
plugin.RuntimeProfiles.LifecycleProfiles = []domain.RuntimeLifecycleProfile{{
|
||||||
|
Key: "run-local", Mode: "local-process",
|
||||||
|
Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus},
|
||||||
|
ActionRefs: domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"},
|
||||||
|
Platforms: []string{"windows"},
|
||||||
|
}}
|
||||||
|
requireManualSCUMRuntimeBindings(&plugin, "run-local")
|
||||||
|
plugin.RuntimeProfiles.ServerDeployments[0].SupportedTargets = []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}
|
||||||
|
if err := svc.store.GamePlugins().Create(plugin); err != nil {
|
||||||
|
t.Fatalf("create SCUM plugin fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := domain.ServerInstance{
|
||||||
|
ID: "scum-generated-guided",
|
||||||
|
PluginID: plugin.ID,
|
||||||
|
PluginVersion: plugin.Version,
|
||||||
|
RunEndpointID: dedicatedRunEndpointID("scum-generated-guided"),
|
||||||
|
Name: "SCUM Generated Guided",
|
||||||
|
OwnerUserID: "owner-scum-generated",
|
||||||
|
State: domain.ServerInstanceStateDraft,
|
||||||
|
ConfigVersion: 1,
|
||||||
|
Deployment: domain.ServerDeploymentDefinition{
|
||||||
|
Mode: domain.ServerDeploymentModeGuided,
|
||||||
|
ProfileKey: "run-local",
|
||||||
|
ServerRoot: `D:\scum-e2e-guided`,
|
||||||
|
CreateInputs: map[string]string{
|
||||||
|
"serverName": "SCUM E2E",
|
||||||
|
"gamePort": "27000",
|
||||||
|
"queryPort": "27015",
|
||||||
|
"maxPlayers": "128",
|
||||||
|
},
|
||||||
|
Revision: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
||||||
|
t.Fatalf("create generated SCUM draft: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get generated Run key: %v", err)
|
||||||
|
}
|
||||||
|
hello := validRunControlHello()
|
||||||
|
hello.RunEndpointID = instance.RunEndpointID
|
||||||
|
hello.RegistrationToken = plainKey
|
||||||
|
hello.ServerInstanceID = instance.ID
|
||||||
|
hello.PluginID = plugin.ID
|
||||||
|
hello.ComponentKind = domain.DistributionComponentRun
|
||||||
|
hello.KeyGeneration = key.Generation
|
||||||
|
hello.Platform = "windows"
|
||||||
|
hello.Architecture = "amd64"
|
||||||
|
hello.CapabilityReport.Capabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus, domain.JobCapabilityDeploymentPlan, domain.JobCapabilitySCUMDeploymentPlan}
|
||||||
|
registered, err := svc.RegisterRunHello(hello)
|
||||||
|
if err != nil || !registered.Accepted {
|
||||||
|
t.Fatalf("register generated SCUM Run: result=%+v err=%v", registered, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stored, err := svc.GetServerInstance(instance.ID)
|
||||||
|
if err != nil || stored.State != domain.ServerInstanceStateInstalling {
|
||||||
|
t.Fatalf("generated SCUM registration should queue install, server=%+v err=%v", stored, err)
|
||||||
|
}
|
||||||
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||||
|
if err != nil || len(jobs) != 1 {
|
||||||
|
t.Fatalf("expected one SCUM install job, jobs=%+v err=%v", jobs, err)
|
||||||
|
}
|
||||||
|
job := jobs[0]
|
||||||
|
if job.Capability != domain.LifecycleCapabilityInstall || job.ExecutionInput.Deployment == nil || job.ExecutionInput.ServerDeploymentPlan == nil {
|
||||||
|
t.Fatalf("expected SCUM install job with protected deployment plan, job=%+v", job)
|
||||||
|
}
|
||||||
|
if job.ExecutionInput.Deployment.CreateInputs["gamePort"] != "27000" || job.ExecutionInput.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||||
|
t.Fatalf("SCUM install job lost create inputs: %+v", job.ExecutionInput.Deployment.CreateInputs)
|
||||||
|
}
|
||||||
|
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, Capabilities: hello.CapabilityReport.Capabilities, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||||
|
if err != nil || !claim.HasJob || claim.Job.ExecutionInput.ServerDeploymentPlan == nil {
|
||||||
|
t.Fatalf("generated SCUM Run should claim install job with frozen plan, claim=%+v err=%v", claim, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func registerDedicatedRunForTest(t *testing.T, svc *CoreService, instance domain.ServerInstance, pluginID string) {
|
func registerDedicatedRunForTest(t *testing.T, svc *CoreService, instance domain.ServerInstance, pluginID string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
|||||||
_ = svc.recordAuditEvent(user.ID, "run.generate.denied", "server-instance", instance.ID, domain.AuditResultDenied, "run generation denied: unsupported target")
|
_ = svc.recordAuditEvent(user.ID, "run.generate.denied", "server-instance", instance.ID, domain.AuditResultDenied, "run generation denied: unsupported target")
|
||||||
return domain.RunDistribution{}, err
|
return domain.RunDistribution{}, err
|
||||||
}
|
}
|
||||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "run.generate.denied"); err != nil {
|
if deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
||||||
return domain.RunDistribution{}, err
|
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "run.generate.denied"); err != nil {
|
||||||
|
return domain.RunDistribution{}, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := svc.promoteLegacyRunBinding(&instance); err != nil {
|
if err := svc.promoteLegacyRunBinding(&instance); err != nil {
|
||||||
return domain.RunDistribution{}, err
|
return domain.RunDistribution{}, err
|
||||||
@@ -485,6 +487,10 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
bindingsComplete, bindingReason := svc.runtimeBindingReadiness(instance.ID)
|
bindingsComplete, bindingReason := svc.runtimeBindingReadiness(instance.ID)
|
||||||
|
runPackageInputsComplete, runPackageReason := bindingsComplete, bindingReason
|
||||||
|
if !deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
||||||
|
runPackageInputsComplete, runPackageReason = true, ""
|
||||||
|
}
|
||||||
builderReady, builderReason := svc.distributionBuilderReadiness()
|
builderReady, builderReason := svc.distributionBuilderReadiness()
|
||||||
dependencyPermissionDeclared := pluginDeclares(plugin, "server.dependencies.manage")
|
dependencyPermissionDeclared := pluginDeclares(plugin, "server.dependencies.manage")
|
||||||
actions := domain.ServerRuntimeActions{
|
actions := domain.ServerRuntimeActions{
|
||||||
@@ -498,9 +504,9 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
|||||||
return domain.RunEndpointStatusOffline
|
return domain.RunEndpointStatusOffline
|
||||||
}(),
|
}(),
|
||||||
Actions: []domain.ServerRuntimeAction{
|
Actions: []domain.ServerRuntimeAction{
|
||||||
runtimeAction("generate-run", "Generate run", pluginDeclares(plugin, "server.run.distribution") && builderReady && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared", fallbackReason(!builderReady, builderReason, bindingReason))),
|
runtimeAction("generate-run", "Generate run", pluginDeclares(plugin, "server.run.distribution") && builderReady && runPackageInputsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared", fallbackReason(!builderReady, builderReason, runPackageReason))),
|
||||||
runtimeAction("download-run", "Download run", hasAvailableRunPackage, "run package has not been generated"),
|
runtimeAction("download-run", "Download run", hasAvailableRunPackage, "run package has not been generated"),
|
||||||
runtimeAction("push-run-update", "Push run update", runRegistered && pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", bindingReason))),
|
runtimeAction("push-run-update", "Push run update", runRegistered && pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && runPackageInputsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", runPackageReason))),
|
||||||
runtimeAction("reset-run-key", "Reset run key", pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared"),
|
runtimeAction("reset-run-key", "Reset run key", pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared"),
|
||||||
runtimeAction("generate-client-manager", "Generate client manager", pluginDeclares(plugin, "server.client-manager.manage") && builderReady && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared", fallbackReason(!builderReady, builderReason, bindingReason))),
|
runtimeAction("generate-client-manager", "Generate client manager", pluginDeclares(plugin, "server.client-manager.manage") && builderReady && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared", fallbackReason(!builderReady, builderReason, bindingReason))),
|
||||||
runtimeAction("download-client-manager", "Download client manager", hasAvailableClientPackage, "client-manager package has not been generated"),
|
runtimeAction("download-client-manager", "Download client manager", hasAvailableClientPackage, "client-manager package has not been generated"),
|
||||||
|
|||||||
@@ -120,6 +120,42 @@ func TestCoreServiceBuildsDedicatedRunInPlatformBuilder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCoreServiceBuildsSCUMGuidedRunWithoutCompleteRuntimeBinding(t *testing.T) {
|
||||||
|
svc := newTestCoreService()
|
||||||
|
plugin := scumDeploymentTestPlugin()
|
||||||
|
plugin.Name = "SCUM"
|
||||||
|
plugin.Version = "0.1.1"
|
||||||
|
plugin.ServerType = "scum"
|
||||||
|
plugin.Status = domain.GamePluginStatusInstalled
|
||||||
|
plugin.SupportedOS = []string{"windows"}
|
||||||
|
plugin.DeclaredPermissions = []string{"server.run.distribution"}
|
||||||
|
plugin.LifecycleActions = domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"}
|
||||||
|
plugin.RuntimeProfiles.LifecycleProfiles = []domain.RuntimeLifecycleProfile{{Key: "run-local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus}, ActionRefs: domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"}, Platforms: []string{"windows"}}}
|
||||||
|
requireManualSCUMRuntimeBindings(&plugin, "run-local")
|
||||||
|
plugin.RuntimeProfiles.ServerDeployments[0].SupportedTargets = []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}
|
||||||
|
if err := svc.store.GamePlugins().Create(plugin); err != nil {
|
||||||
|
t.Fatalf("create SCUM plugin: %v", err)
|
||||||
|
}
|
||||||
|
session := createServiceUserAndLogin(t, svc, domain.User{ID: "scum-run-builder-owner", DisplayName: "SCUM Run Builder Owner", Email: "scum-run-builder@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||||
|
created, err := svc.CreateServerInstanceWorkflowForSession(session, domain.ServerLifecycleCreate{
|
||||||
|
ID: "scum-guided-run-build", PluginID: plugin.ID, Name: "SCUM Guided Run Build", IdempotencyKey: "scum-guided-run-build-create", ProfileKey: "run-local",
|
||||||
|
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: `D:\scum-guided-run-build`, CreateInputs: map[string]string{"serverName": "Moon", "gamePort": "27000", "queryPort": "27015", "maxPlayers": "128"}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create SCUM guided draft: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.runtimeBindingForServer(created.Instance.ID); !errors.Is(err, repo.ErrNotFound) {
|
||||||
|
t.Fatalf("SCUM guided draft should not need a complete runtime binding before run build: %v", err)
|
||||||
|
}
|
||||||
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: created.Instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "scum-guided-run-build"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate SCUM guided run without runtime binding: %v", err)
|
||||||
|
}
|
||||||
|
if distribution.RunEndpointID != dedicatedRunEndpointID(created.Instance.ID) {
|
||||||
|
t.Fatalf("expected generated run endpoint to be promoted, got %+v", distribution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCoreServicePromotesLegacyRunBindingBeforeDistributionBuild(t *testing.T) {
|
func TestCoreServicePromotesLegacyRunBindingBeforeDistributionBuild(t *testing.T) {
|
||||||
svc, session, instance := newDistributionTestFixture(t)
|
svc, session, instance := newDistributionTestFixture(t)
|
||||||
legacyEndpointID := instance.RunEndpointID
|
legacyEndpointID := instance.RunEndpointID
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
|
|||||||
State: job.State,
|
State: job.State,
|
||||||
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
|
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
|
||||||
ResultRef: job.ResultRef,
|
ResultRef: job.ResultRef,
|
||||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment)},
|
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)},
|
||||||
LeaseToken: leaseToken,
|
LeaseToken: leaseToken,
|
||||||
Attempt: job.Attempt,
|
Attempt: job.Attempt,
|
||||||
FencingToken: fencingToken,
|
FencingToken: fencingToken,
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ func scumDeploymentCapabilityRequired(plugin domain.GamePlugin, definition domai
|
|||||||
return plugin.ID == scumPluginID && (definition.Mode == domain.ServerDeploymentModeGuided || definition.Mode == domain.ServerDeploymentModeExisting)
|
return plugin.ID == scumPluginID && (definition.Mode == domain.ServerDeploymentModeGuided || definition.Mode == domain.ServerDeploymentModeExisting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deploymentNeedsCompleteRuntimeBinding(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) bool {
|
||||||
|
return !scumDeploymentCapabilityRequired(plugin, definition)
|
||||||
|
}
|
||||||
|
|
||||||
func validateSCUMDeploymentTarget(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition, endpoint domain.RunEndpoint) error {
|
func validateSCUMDeploymentTarget(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition, endpoint domain.RunEndpoint) error {
|
||||||
if !scumDeploymentCapabilityRequired(plugin, definition) {
|
if !scumDeploymentCapabilityRequired(plugin, definition) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ func scumDeploymentTestPlugin() domain.GamePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireManualSCUMRuntimeBindings(plugin *domain.GamePlugin, profileKey string) {
|
||||||
|
plugin.RuntimeProfiles.Discovery = []domain.RuntimeDiscoveryProbe{
|
||||||
|
{Key: "steamcmd", Kind: "command.version", TargetKey: "steamcmd", Required: true},
|
||||||
|
{Key: "scum-install", Kind: "file.exists", TargetKey: "server/install-root", Required: true, Platforms: []string{"windows"}},
|
||||||
|
}
|
||||||
|
plugin.RuntimeProfiles.DependencyProbes = []domain.RuntimeDependencyProbe{{Key: "steamcmd", Kind: "command.version", TargetKey: "steamcmd", Required: true, Platforms: []string{"windows"}}}
|
||||||
|
plugin.RuntimeProfiles.LogSources = []domain.RuntimeLogSource{
|
||||||
|
{Key: "scum-console-stdout", Kind: "process.stdout", TargetKey: "scum/server-process", StreamKey: "scum.console.stdout", CursorKind: "sequence", RetentionDays: 30},
|
||||||
|
{Key: "scum-server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "scum.server", CursorKind: "fingerprint", RetentionDays: 90},
|
||||||
|
}
|
||||||
|
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite}}}
|
||||||
|
for i := range plugin.RuntimeProfiles.LifecycleProfiles {
|
||||||
|
if plugin.RuntimeProfiles.LifecycleProfiles[i].Key == profileKey {
|
||||||
|
plugin.RuntimeProfiles.LifecycleProfiles[i].TransportKeys = []string{"server-files"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSCUMDeploymentPlanSeparatesInstallAndAdopt(t *testing.T) {
|
func TestSCUMDeploymentPlanSeparatesInstallAndAdopt(t *testing.T) {
|
||||||
plugin := scumDeploymentTestPlugin()
|
plugin := scumDeploymentTestPlugin()
|
||||||
definition := domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, CreateInputs: map[string]string{"serverName": "Alpha", "gamePort": "7777", "queryPort": "27015", "maxPlayers": "64"}}
|
definition := domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, CreateInputs: map[string]string{"serverName": "Alpha", "gamePort": "7777", "queryPort": "27015", "maxPlayers": "64"}}
|
||||||
|
|||||||
@@ -131,22 +131,13 @@ func (svc *CoreService) deployServerInstance(command domain.ServerLifecycleComma
|
|||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
instance.Deployment = applyPluginCreateDefaults(plugin, instance.Deployment)
|
instance.Deployment = applyPluginCreateDefaults(plugin, instance.Deployment)
|
||||||
if !containsString(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan) {
|
|
||||||
return domain.ServerLifecycleResult{}, validationError("run endpoint missing required capability: deployment.plan.v1")
|
|
||||||
}
|
|
||||||
if scumDeploymentCapabilityRequired(plugin, instance.Deployment) && !containsString(endpoint.Capabilities, domain.JobCapabilitySCUMDeploymentPlan) {
|
|
||||||
return domain.ServerLifecycleResult{}, validationError("run endpoint missing required capability: deployment.scum.v1")
|
|
||||||
}
|
|
||||||
if err := validateSCUMDeploymentTarget(plugin, instance.Deployment, endpoint); err != nil {
|
if err := validateSCUMDeploymentTarget(plugin, instance.Deployment, endpoint); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if requiredShellCapability := deploymentShellCapability(instance.Deployment.Shell); requiredShellCapability != "" && !containsString(endpoint.Capabilities, requiredShellCapability) {
|
|
||||||
return domain.ServerLifecycleResult{}, validationError("run endpoint policy does not allow selected command shell")
|
|
||||||
}
|
|
||||||
if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); err != nil {
|
if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
|
if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, domain.ServerLifecycleActionCreate); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if err := validateScumDeploymentInputs(plugin, instance.Deployment); err != nil {
|
if err := validateScumDeploymentInputs(plugin, instance.Deployment); err != nil {
|
||||||
@@ -158,7 +149,7 @@ func (svc *CoreService) deployServerInstance(command domain.ServerLifecycleComma
|
|||||||
if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, command.IdempotencyKey, instance.ID, domain.LifecycleCapabilityInstall); err != nil {
|
if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, command.IdempotencyKey, instance.ID, domain.LifecycleCapabilityInstall); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(instance.Deployment.ProfileKey) != "" {
|
if strings.TrimSpace(instance.Deployment.ProfileKey) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
||||||
binding, bindingErr := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: instance.Deployment.ProfileKey, Bindings: instance.Deployment.RuntimeBindings}, true)
|
binding, bindingErr := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: instance.Deployment.ProfileKey, Bindings: instance.Deployment.RuntimeBindings}, true)
|
||||||
if bindingErr != nil {
|
if bindingErr != nil {
|
||||||
return domain.ServerLifecycleResult{}, bindingErr
|
return domain.ServerLifecycleResult{}, bindingErr
|
||||||
|
|||||||
@@ -65,6 +65,49 @@ func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCoreServiceSCUMGuidedDeployUsesScopedCapabilitiesAndDispatchesFrozenPlan(t *testing.T) {
|
||||||
|
svc := newTestCoreService()
|
||||||
|
runHello := validRunControlHello()
|
||||||
|
runHello.RunEndpointID = "run-scum-guided"
|
||||||
|
runHello.Platform = "windows"
|
||||||
|
runHello.Architecture = "amd64"
|
||||||
|
runHello.CapabilityReport.Capabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.JobCapabilityDeploymentPlan, domain.JobCapabilitySCUMDeploymentPlan}
|
||||||
|
runHello.CapabilityReport.Fingerprint = "cap-scum-guided"
|
||||||
|
session, err := svc.RegisterRunHello(runHello)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register scoped SCUM Run: %v", err)
|
||||||
|
}
|
||||||
|
plugin := scumDeploymentTestPlugin()
|
||||||
|
plugin.Name = "SCUM"
|
||||||
|
plugin.Version = "1.0.0"
|
||||||
|
plugin.ServerType = "scum"
|
||||||
|
plugin.Status = domain.GamePluginStatusInstalled
|
||||||
|
plugin.RequiredRunCapabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, "process.restart", "files.list", "files.patch", domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall, "artifacts.read", "artifacts.write", "ai.invoke"}
|
||||||
|
plugin.LifecycleActions = domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json"}
|
||||||
|
plugin.RuntimeProfiles.LifecycleProfiles = []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop}}}
|
||||||
|
requireManualSCUMRuntimeBindings(&plugin, "local")
|
||||||
|
plugin.RuntimeProfiles.ServerDeployments[0].SupportedTargets = []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}
|
||||||
|
if err := svc.store.GamePlugins().Create(plugin); err != nil {
|
||||||
|
t.Fatalf("create SCUM plugin fixture: %v", err)
|
||||||
|
}
|
||||||
|
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "scum-guided-owner", DisplayName: "SCUM Guided Owner", Email: "scum-guided@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||||
|
|
||||||
|
created, err := svc.CreateServerInstanceWorkflowForSession(ownerSession, domain.ServerLifecycleCreate{
|
||||||
|
ID: "scum-guided-scoped", PluginID: plugin.ID, RunEndpointID: "run-scum-guided", Name: "SCUM Guided", IdempotencyKey: "scum-guided-scoped-create", ProfileKey: "local",
|
||||||
|
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: "C:\\scum-guided", CreateInputs: map[string]string{"serverName": "Moon", "gamePort": "27000", "queryPort": "27015", "maxPlayers": "128"}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SCUM guided deployment should not require unrelated manifest capabilities: %v", err)
|
||||||
|
}
|
||||||
|
if created.Job.ExecutionInput.ServerDeploymentPlan == nil || created.Job.ExecutionInput.ServerDeploymentPlan.SteamAppID != "3792580" {
|
||||||
|
t.Fatalf("expected frozen SCUM deployment plan on queued job, got %+v", created.Job.ExecutionInput)
|
||||||
|
}
|
||||||
|
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-scum-guided", SessionToken: session.SessionToken, Capabilities: []string{domain.LifecycleCapabilityInstall}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||||
|
if err != nil || !claim.HasJob || claim.Job.ExecutionInput.ServerDeploymentPlan == nil {
|
||||||
|
t.Fatalf("claimed SCUM install must include frozen deployment plan, claim=%+v err=%v", claim, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMergeDeploymentPreservesOmittedShellAndClearsOnlyExplicitFields(t *testing.T) {
|
func TestMergeDeploymentPreservesOmittedShellAndClearsOnlyExplicitFields(t *testing.T) {
|
||||||
current := domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/game", StartCommand: "./start", StopCommand: "./stop", Shell: domain.ServerCommandShellCmd}
|
current := domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/game", StartCommand: "./start", StopCommand: "./stop", Shell: domain.ServerCommandShellCmd}
|
||||||
preserved := mergeDeploymentDefinition(current, domain.ServerDeploymentUpdate{Mode: domain.ServerDeploymentModeCustom})
|
preserved := mergeDeploymentDefinition(current, domain.ServerDeploymentUpdate{Mode: domain.ServerDeploymentModeCustom})
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
|||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
var binding domain.RuntimeBinding
|
var binding domain.RuntimeBinding
|
||||||
if strings.TrimSpace(create.ProfileKey) != "" {
|
if strings.TrimSpace(create.ProfileKey) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
||||||
var bindingErr error
|
var bindingErr error
|
||||||
binding, bindingErr = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
|
binding, bindingErr = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
|
||||||
if bindingErr != nil {
|
if bindingErr != nil {
|
||||||
@@ -121,7 +121,7 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ServerLifecycleResult{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
return domain.ServerLifecycleResult{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
||||||
}
|
}
|
||||||
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
|
if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, domain.ServerLifecycleActionCreate); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
|
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
|
||||||
@@ -132,23 +132,14 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
|||||||
instance.DeploymentProjection.PreflightState = "queued"
|
instance.DeploymentProjection.PreflightState = "queued"
|
||||||
instance.DeploymentProjection.UpdatedAt = stamp
|
instance.DeploymentProjection.UpdatedAt = stamp
|
||||||
}
|
}
|
||||||
if instance.Deployment.Mode != "" && !containsString(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan) {
|
|
||||||
return domain.ServerLifecycleResult{}, validationError("run endpoint missing required capability: deployment.plan.v1")
|
|
||||||
}
|
|
||||||
if scumDeploymentCapabilityRequired(plugin, instance.Deployment) && !containsString(endpoint.Capabilities, domain.JobCapabilitySCUMDeploymentPlan) {
|
|
||||||
return domain.ServerLifecycleResult{}, validationError("run endpoint missing required capability: deployment.scum.v1")
|
|
||||||
}
|
|
||||||
if err := validateSCUMDeploymentTarget(plugin, instance.Deployment, endpoint); err != nil {
|
if err := validateSCUMDeploymentTarget(plugin, instance.Deployment, endpoint); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if requiredShellCapability := deploymentShellCapability(instance.Deployment.Shell); requiredShellCapability != "" && !containsString(endpoint.Capabilities, requiredShellCapability) {
|
|
||||||
return domain.ServerLifecycleResult{}, validationError("run endpoint policy does not allow selected command shell")
|
|
||||||
}
|
|
||||||
if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
|
if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
var binding domain.RuntimeBinding
|
var binding domain.RuntimeBinding
|
||||||
hasBinding := strings.TrimSpace(create.ProfileKey) != ""
|
hasBinding := strings.TrimSpace(create.ProfileKey) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment)
|
||||||
if hasBinding {
|
if hasBinding {
|
||||||
binding, err = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
|
binding, err = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -260,7 +251,7 @@ func (svc *CoreService) dispatchExistingServerLifecycle(command domain.ServerLif
|
|||||||
if err := validateLifecycleActionRef(plugin, action); err != nil {
|
if err := validateLifecycleActionRef(plugin, action); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
|
if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, action); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
if action == domain.ServerLifecycleActionStart {
|
if action == domain.ServerLifecycleActionStart {
|
||||||
@@ -276,8 +267,10 @@ func (svc *CoreService) dispatchExistingServerLifecycle(command domain.ServerLif
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := svc.requireCompleteRuntimeBindings(instance.OwnerUserID, instance.ID, "server.lifecycle."+string(action)+".denied"); err != nil {
|
if deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
||||||
return domain.ServerLifecycleResult{}, err
|
if err := svc.requireCompleteRuntimeBindings(instance.OwnerUserID, instance.ID, "server.lifecycle."+string(action)+".denied"); err != nil {
|
||||||
|
return domain.ServerLifecycleResult{}, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); err != nil {
|
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
@@ -517,3 +510,33 @@ func lifecycleJobID(serverInstanceID string, action domain.ServerLifecycleAction
|
|||||||
sum := sha256.Sum256([]byte(idempotencyKey))
|
sum := sha256.Sum256([]byte(idempotencyKey))
|
||||||
return fmt.Sprintf("server-lifecycle:%s:%s:%s", serverInstanceID, action, hex.EncodeToString(sum[:8]))
|
return fmt.Sprintf("server-lifecycle:%s:%s:%s", serverInstanceID, action, hex.EncodeToString(sum[:8]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateServerInstanceLifecycleDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint, action domain.ServerLifecycleAction) error {
|
||||||
|
if requiredShellCapability := deploymentShellCapability(instance.Deployment.Shell); requiredShellCapability != "" && !containsString(endpoint.Capabilities, requiredShellCapability) {
|
||||||
|
return validationError("run endpoint policy does not allow selected command shell")
|
||||||
|
}
|
||||||
|
return validator.ValidateServerInstanceDependenciesForCapabilities(instance, plugin, endpoint, lifecycleRequiredRunCapabilities(instance, plugin, action))
|
||||||
|
}
|
||||||
|
|
||||||
|
func lifecycleRequiredRunCapabilities(instance domain.ServerInstance, plugin domain.GamePlugin, action domain.ServerLifecycleAction) []string {
|
||||||
|
required := append([]string(nil), domain.LifecycleCapabilityForAction(action))
|
||||||
|
if instance.Deployment.Mode != "" {
|
||||||
|
required = append(required, domain.JobCapabilityDeploymentPlan)
|
||||||
|
if scumDeploymentCapabilityRequired(plugin, instance.Deployment) {
|
||||||
|
required = append(required, domain.JobCapabilitySCUMDeploymentPlan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compactUniqueStrings(required)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compactUniqueStrings(values []string) []string {
|
||||||
|
out := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" || containsString(out, value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -1006,6 +1006,10 @@ func ValidateServerInstanceUpdate(update domain.ServerInstanceUpdate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) error {
|
func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) error {
|
||||||
|
return ValidateServerInstanceDependenciesForCapabilities(instance, plugin, endpoint, plugin.RequiredRunCapabilities)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateServerInstanceDependenciesForCapabilities(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint, requiredRunCapabilities []string) error {
|
||||||
var violations []string
|
var violations []string
|
||||||
if plugin.ID == "" {
|
if plugin.ID == "" {
|
||||||
violations = append(violations, "plugin is required")
|
violations = append(violations, "plugin is required")
|
||||||
@@ -1029,7 +1033,7 @@ func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin d
|
|||||||
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
|
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
|
||||||
violations = append(violations, "run endpoint must be online or degraded")
|
violations = append(violations, "run endpoint must be online or degraded")
|
||||||
}
|
}
|
||||||
missing := MissingCapabilities(endpoint.Capabilities, plugin.RequiredRunCapabilities)
|
missing := MissingCapabilities(endpoint.Capabilities, requiredRunCapabilities)
|
||||||
if len(missing) > 0 {
|
if len(missing) > 0 {
|
||||||
violations = append(violations, "run endpoint missing required capabilities: "+strings.Join(missing, ", "))
|
violations = append(violations, "run endpoint missing required capabilities: "+strings.Join(missing, ", "))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user