Files
browser/platform/service/server_deployment.go
T

253 lines
11 KiB
Go

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 update.RunEndpointID != "" {
if _, err := svc.store.RunEndpoints().Get(update.RunEndpointID); err != nil {
return domain.ServerDeploymentView{}, err
}
instance.RunEndpointID = update.RunEndpointID
}
instance.Deployment = definition
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 err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, domain.ServerLifecycleActionCreate); 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) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
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 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
}