package service import ( "errors" "strings" "time" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) func (svc *CoreService) GetServerDeploymentForSession(sessionID, serverInstanceID string) (domain.ServerDeploymentView, error) { if _, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID); err != nil { return domain.ServerDeploymentView{}, err } instance, err := svc.store.ServerInstances().Get(serverInstanceID) if err != nil { return domain.ServerDeploymentView{}, err } return svc.deploymentView(instance) } func (svc *CoreService) RevealServerDeploymentForSession(sessionID, serverInstanceID string) (domain.ServerDeploymentReveal, error) { _, instance, err := svc.requireServerOwner(sessionID, serverInstanceID) if err != nil { return domain.ServerDeploymentReveal{}, err } definition := instance.Deployment return domain.ServerDeploymentReveal{ServerInstanceID: instance.ID, ServerRoot: definition.ServerRoot, WorkingDirectory: definition.WorkingDirectory, InstallCommand: definition.InstallCommand, StartCommand: definition.StartCommand, StopCommand: definition.StopCommand, StatusCommand: definition.StatusCommand}, nil } func (svc *CoreService) UpdateServerDeploymentForSession(sessionID, serverInstanceID string, update domain.ServerDeploymentUpdate) (domain.ServerDeploymentView, error) { _, instance, err := svc.requireServerOwner(sessionID, serverInstanceID) if err != nil { return domain.ServerDeploymentView{}, err } 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() if err := validator.ValidateServerDeploymentDefinition(definition); err != nil { return domain.ServerDeploymentView{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.ServerDeploymentView{}, err } definition = applyPluginCreateDefaults(plugin, definition) if err := validator.ValidatePluginCreateInputs(plugin.CreateFields, definition.CreateInputs); err != nil { return domain.ServerDeploymentView{}, err } if err := validateScumDeploymentInputs(plugin, definition); err != nil { return domain.ServerDeploymentView{}, err } if update.RunEndpointID != "" { if _, err := svc.store.RunEndpoints().Get(update.RunEndpointID); err != nil { return domain.ServerDeploymentView{}, err } instance.RunEndpointID = update.RunEndpointID } instance.Deployment = definition operation := "install" if definition.Mode == domain.ServerDeploymentModeExisting { operation = "adopt" } if plan, planErr := scumDeploymentPlan(plugin, definition, operation); planErr != nil { return domain.ServerDeploymentView{}, planErr } else if plan != nil { instance.DeploymentProjection = scumDeploymentProjection(plan, operation, definition.UpdatedAt) } else { instance.DeploymentProjection = domain.ServerDeploymentProjection{} } instance.UpdatedAt = definition.UpdatedAt if err := validator.ValidateServerInstance(instance); err != nil { return domain.ServerDeploymentView{}, err } if err := svc.store.ServerInstances().Update(instance); err != nil { return domain.ServerDeploymentView{}, err } return svc.deploymentView(instance) } func (svc *CoreService) DeployServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) { if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil { return domain.ServerLifecycleResult{}, err } return svc.deployServerInstance(command) } // deployServerInstance is the platform-owned transition from a saved deployment // definition to one fenced install job. Callers must already have established // the authority to act for the server. func (svc *CoreService) deployServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) { if err := validator.ValidateServerLifecycleCommand(command); err != nil { return domain.ServerLifecycleResult{}, err } instance, err := svc.store.ServerInstances().Get(command.ServerInstanceID) if err != nil { return domain.ServerLifecycleResult{}, err } if instance.State != domain.ServerInstanceStateDraft && instance.State != domain.ServerInstanceStateFailed { return domain.ServerLifecycleResult{}, validationError("server instance is not deployable") } if instance.ConfigVersion != command.ExpectedConfigVersion { return domain.ServerLifecycleResult{}, validationError("expectedConfigVersion must match server instance") } if instance.Deployment.Mode == "" { return domain.ServerLifecycleResult{}, validationError("deployment definition is required") } if strings.TrimSpace(instance.RunEndpointID) == "" { return domain.ServerLifecycleResult{}, validationError("run endpoint must be selected before deployment") } if _, err := svc.store.RunEndpoints().Get(instance.RunEndpointID); err != nil { if errors.Is(err, repo.ErrNotFound) && strings.TrimSpace(instance.DeploymentTargetID) != "" { return domain.ServerLifecycleResult{}, validationError("dedicated Run must register before deployment") } return domain.ServerLifecycleResult{}, err } plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID) if err != nil { return domain.ServerLifecycleResult{}, err } 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 { 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 { return domain.ServerLifecycleResult{}, err } if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil { return domain.ServerLifecycleResult{}, err } if err := validateScumDeploymentInputs(plugin, instance.Deployment); err != nil { return domain.ServerLifecycleResult{}, err } if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityInstall); err != nil { return domain.ServerLifecycleResult{}, err } if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, command.IdempotencyKey, instance.ID, domain.LifecycleCapabilityInstall); err != nil { return domain.ServerLifecycleResult{}, err } if strings.TrimSpace(instance.Deployment.ProfileKey) != "" { binding, bindingErr := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: instance.Deployment.ProfileKey, Bindings: instance.Deployment.RuntimeBindings}, true) if bindingErr != nil { return domain.ServerLifecycleResult{}, bindingErr } if existing, existingErr := svc.runtimeBindingForServer(instance.ID); existingErr == nil { binding.CreatedAt = existing.CreatedAt if err := svc.store.RuntimeBindings().Update(binding); err != nil { return domain.ServerLifecycleResult{}, err } } else if errors.Is(existingErr, repo.ErrNotFound) { if err := svc.store.RuntimeBindings().Create(binding); err != nil { return domain.ServerLifecycleResult{}, err } } else { return domain.ServerLifecycleResult{}, existingErr } } instance.State = domain.ServerInstanceStateInstalling instance.UpdatedAt = svc.now() if instance.DeploymentProjection.TemplateKey != "" { instance.DeploymentProjection.State = "queued" instance.DeploymentProjection.PreflightState = "queued" instance.DeploymentProjection.UpdatedAt = instance.UpdatedAt } if err := svc.store.ServerInstances().Update(instance); err != nil { return domain.ServerLifecycleResult{}, err } job, err := svc.dispatchLifecycleJob(instance, domain.ServerLifecycleActionCreate, command.IdempotencyKey) if err != nil { return domain.ServerLifecycleResult{}, err } return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance, Job: job}), nil } func deploymentShellCapability(shell domain.ServerCommandShell) string { switch shell { case domain.ServerCommandShellPosix: return domain.JobCapabilityDeploymentShellPosix case domain.ServerCommandShellPowerShell: return domain.JobCapabilityDeploymentShellPowerShell case domain.ServerCommandShellCmd: return domain.JobCapabilityDeploymentShellCmd default: return "" } } 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 } if update.RuntimeBindings != nil { definition.RuntimeBindings = domain.CopyStringMap(update.RuntimeBindings) } if update.CreateInputs != nil { definition.CreateInputs = domain.CopyStringMap(update.CreateInputs) } if update.ServerRoot != "" { definition.ServerRoot = update.ServerRoot } if update.WorkingDirectory != "" { definition.WorkingDirectory = update.WorkingDirectory } if update.InstallCommand != "" { definition.InstallCommand = update.InstallCommand } if update.StartCommand != "" { definition.StartCommand = update.StartCommand } if update.StopCommand != "" { definition.StopCommand = update.StopCommand } if update.StatusCommand != "" { definition.StatusCommand = update.StatusCommand } if update.ShellSet { definition.Shell = update.Shell } return definition } func (svc *CoreService) deploymentView(instance domain.ServerInstance) (domain.ServerDeploymentView, error) { definition := instance.Deployment view := domain.ServerDeploymentView{ ServerInstanceID: instance.ID, Mode: definition.Mode, ProfileKey: definition.ProfileKey, CreateInputs: domain.CopyStringMap(definition.CreateInputs), ServerRootConfigured: definition.ServerRoot != "", WorkingDirectoryConfigured: definition.WorkingDirectory != "", InstallCommandConfigured: definition.InstallCommand != "", StartCommandConfigured: definition.StartCommand != "", StopCommandConfigured: definition.StopCommand != "", StatusCommandConfigured: definition.StatusCommand != "", Shell: definition.Shell, Revision: definition.Revision, UpdatedAt: definition.UpdatedAt, Projection: instance.DeploymentProjection, } jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID}) if err != nil { return domain.ServerDeploymentView{}, err } var latestDispatchAt time.Time for _, job := range jobs { if job.ExecutionInput.Deployment == nil || (view.LatestDispatch != nil && !job.CreatedAt.After(latestDispatchAt)) { continue } receipt := job.ExecutionResult.DeploymentReceipt confirmed := receipt != nil && receipt.Revision == job.ExecutionInput.Deployment.Revision && receipt.Action == job.ExecutionInput.LifecycleOperation view.LatestDispatch = &domain.ServerDeploymentDispatchEvidence{JobID: job.ID, JobState: job.State, DeploymentRevision: job.ExecutionInput.Deployment.Revision, DeploymentDefinitionIncluded: true, RunConfirmed: confirmed} latestDispatchAt = job.CreatedAt } return domain.CopyServerDeploymentView(view), nil }