feat: support custom server deployment drafts
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"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 deploymentView(instance), 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")
|
||||
}
|
||||
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
|
||||
}
|
||||
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.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 deploymentView(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) DeployServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
||||
if err := validator.ValidateServerLifecycleCommand(command); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); 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")
|
||||
}
|
||||
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if !containsString(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan) {
|
||||
return domain.ServerLifecycleResult{}, validationError("run endpoint missing required capability: deployment.plan.v1")
|
||||
}
|
||||
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 := 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 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)
|
||||
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
|
||||
}
|
||||
definition.Shell = update.Shell
|
||||
return definition
|
||||
}
|
||||
|
||||
func deploymentView(instance domain.ServerInstance) domain.ServerDeploymentView {
|
||||
definition := instance.Deployment
|
||||
return domain.CopyServerDeploymentView(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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user