556 lines
22 KiB
Go
556 lines
22 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/repo"
|
|
"browser.local/platform/validator"
|
|
)
|
|
|
|
func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) {
|
|
create = domain.CopyServerLifecycleCreate(create)
|
|
if err := validator.ValidateServerLifecycleCreate(create); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
|
|
plugin, err := svc.store.GamePlugins().Get(create.PluginID)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, fmt.Errorf("get plugin dependency: %w", err)
|
|
}
|
|
if plugin.Status != domain.GamePluginStatusInstalled {
|
|
return domain.ServerLifecycleResult{}, validationError("plugin must be installed")
|
|
}
|
|
create.Deployment = applyPluginCreateDefaults(plugin, create.Deployment)
|
|
if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if create.Deployment.Mode != "" {
|
|
if err := validator.ValidatePluginCreateInputs(plugin.CreateFields, create.Deployment.CreateInputs); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
}
|
|
|
|
stamp := svc.now()
|
|
instance := domain.ServerInstance{
|
|
ID: create.ID,
|
|
PluginID: create.PluginID,
|
|
PluginVersion: plugin.Version,
|
|
RunEndpointID: create.RunEndpointID,
|
|
Name: create.Name,
|
|
OwnerUserID: create.OwnerUserID,
|
|
State: domain.ServerInstanceStateInstalling,
|
|
ConfigVersion: 1,
|
|
ConfigKey: "server.properties",
|
|
CreatedAt: stamp,
|
|
UpdatedAt: stamp,
|
|
Deployment: create.Deployment,
|
|
}
|
|
if strings.TrimSpace(create.DeploymentTargetID) != "" {
|
|
instance.DeploymentTargetID = create.DeploymentTargetID
|
|
instance.RunEndpointID = dedicatedRunEndpointID(create.ID)
|
|
instance.State = domain.ServerInstanceStateDraft
|
|
}
|
|
if instance.Deployment.Mode != "" {
|
|
if strings.TrimSpace(create.ProfileKey) != "" {
|
|
instance.Deployment.ProfileKey = create.ProfileKey
|
|
}
|
|
instance.Deployment.RuntimeBindings = domain.CopyStringMap(create.Bindings)
|
|
instance.Deployment.Revision = maxInt(1, instance.Deployment.Revision)
|
|
instance.Deployment.UpdatedAt = stamp
|
|
}
|
|
instance.ConfigContent = buildLogicalServerConfig(instance)
|
|
instance.ConfigChecksum = validator.BytesChecksum([]byte(instance.ConfigContent))
|
|
instance.ConfigUpdatedAt = stamp
|
|
if strings.TrimSpace(create.DeploymentTargetID) != "" {
|
|
target, targetErr := svc.GetRunEndpoint(create.DeploymentTargetID)
|
|
if targetErr != nil {
|
|
return domain.ServerLifecycleResult{}, fmt.Errorf("get deployment target dependency: %w", targetErr)
|
|
}
|
|
if err := svc.validateRunnableEndpoint(target, domain.JobCapabilityDistributionBuild); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if err := validator.ValidateServerInstance(instance); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
var binding domain.RuntimeBinding
|
|
if strings.TrimSpace(create.ProfileKey) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) {
|
|
var bindingErr error
|
|
binding, bindingErr = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
|
|
if bindingErr != nil {
|
|
return domain.ServerLifecycleResult{}, bindingErr
|
|
}
|
|
}
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if binding.ID != "" {
|
|
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
}
|
|
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance}), nil
|
|
}
|
|
if strings.TrimSpace(create.RunEndpointID) == "" {
|
|
instance.State = domain.ServerInstanceStateDraft
|
|
}
|
|
if err := validator.ValidateServerInstance(instance); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if strings.TrimSpace(create.RunEndpointID) == "" {
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance}), nil
|
|
}
|
|
endpoint, err := svc.GetRunEndpoint(create.RunEndpointID)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
|
}
|
|
bootstrapAction := deploymentBootstrapLifecycleAction(plugin, instance)
|
|
if err := validateLifecycleActionRef(plugin, bootstrapAction); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, bootstrapAction); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(bootstrapAction)); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(bootstrapAction)); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
var binding domain.RuntimeBinding
|
|
hasBinding := strings.TrimSpace(create.ProfileKey) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment)
|
|
if hasBinding {
|
|
binding, err = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
}
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if hasBinding {
|
|
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
}
|
|
|
|
job, err := svc.dispatchLifecycleJob(instance, bootstrapAction, create.IdempotencyKey)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{
|
|
Accepted: true,
|
|
Action: domain.ServerLifecycleActionCreate,
|
|
Instance: instance,
|
|
Job: job,
|
|
}), nil
|
|
}
|
|
|
|
func generatedRunEndpointID(serverInstanceID string) string {
|
|
return "server-run-" + serverInstanceID
|
|
}
|
|
|
|
// dedicatedRunEndpointID remains a compatibility helper for legacy fixtures.
|
|
// Production flow uses generatedRunEndpointID only when building a package and
|
|
// attaches the active endpoint from the first authenticated Run heartbeat.
|
|
func dedicatedRunEndpointID(serverInstanceID string) string {
|
|
return generatedRunEndpointID(serverInstanceID)
|
|
}
|
|
|
|
func runEndpointIDForDistribution(instance domain.ServerInstance) string {
|
|
if endpointID := strings.TrimSpace(instance.RunEndpointID); endpointID != "" {
|
|
return endpointID
|
|
}
|
|
return generatedRunEndpointID(instance.ID)
|
|
}
|
|
|
|
func (svc *CoreService) CreateServerInstanceWorkflowForSession(sessionID string, create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) {
|
|
user, err := svc.GetCurrentUser(sessionID)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if strings.TrimSpace(create.OwnerUserID) == "" {
|
|
create.OwnerUserID = user.ID
|
|
}
|
|
if !isPlatformAdmin(user) && create.OwnerUserID != user.ID {
|
|
return domain.ServerLifecycleResult{}, ErrForbidden
|
|
}
|
|
return svc.CreateServerInstanceWorkflow(create)
|
|
}
|
|
|
|
func (svc *CoreService) StartServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
|
return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStart, []domain.ServerInstanceState{
|
|
domain.ServerInstanceStateReady,
|
|
domain.ServerInstanceStateStopped,
|
|
domain.ServerInstanceStateFailed,
|
|
})
|
|
}
|
|
|
|
func (svc *CoreService) StartServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
|
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
return svc.StartServerInstance(command)
|
|
}
|
|
|
|
func (svc *CoreService) StopServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
|
return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStop, []domain.ServerInstanceState{
|
|
domain.ServerInstanceStateRunning,
|
|
})
|
|
}
|
|
|
|
func (svc *CoreService) StopServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
|
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
return svc.StopServerInstance(command)
|
|
}
|
|
|
|
func (svc *CoreService) QueryServerInstanceProcessForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
|
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStatus, []domain.ServerInstanceState{
|
|
domain.ServerInstanceStateReady,
|
|
domain.ServerInstanceStateStopped,
|
|
domain.ServerInstanceStateRunning,
|
|
domain.ServerInstanceStateFailed,
|
|
})
|
|
}
|
|
|
|
func (svc *CoreService) dispatchExistingServerLifecycle(command domain.ServerLifecycleCommand, action domain.ServerLifecycleAction, allowedStates []domain.ServerInstanceState) (domain.ServerLifecycleResult, error) {
|
|
command = domain.CopyServerLifecycleCommand(command)
|
|
if err := validator.ValidateServerLifecycleCommand(command); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if err := validator.ValidateServerLifecycleAction(action); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
|
|
instance, err := svc.store.ServerInstances().Get(command.ServerInstanceID)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if instance.ConfigVersion != command.ExpectedConfigVersion {
|
|
return domain.ServerLifecycleResult{}, validationError("expectedConfigVersion must match server instance")
|
|
}
|
|
if !serverStateAllowed(instance.State, allowedStates) {
|
|
return domain.ServerLifecycleResult{}, validationError(fmt.Sprintf("server instance state %q cannot %s", instance.State, action))
|
|
}
|
|
|
|
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if err := validateLifecycleActionRef(plugin, action); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, action); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if action == domain.ServerLifecycleActionStart {
|
|
binding, bindingErr := svc.runtimeBindingForServer(instance.ID)
|
|
if bindingErr != nil && !errors.Is(bindingErr, repo.ErrNotFound) {
|
|
return domain.ServerLifecycleResult{}, bindingErr
|
|
}
|
|
if bindingErr == nil {
|
|
if profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey); exists && len(profile.DLLExtensionRefs) > 0 {
|
|
if _, extensionErr := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint); extensionErr != nil {
|
|
return domain.ServerLifecycleResult{}, extensionErr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
if action == domain.ServerLifecycleActionStart {
|
|
instance.LifecycleProcessID = ""
|
|
instance.LifecycleObservationSeq = 0
|
|
instance.LifecycleObservedAt = time.Time{}
|
|
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
}
|
|
|
|
job, err := svc.dispatchLifecycleJob(instance, action, command.IdempotencyKey)
|
|
if err != nil {
|
|
return domain.ServerLifecycleResult{}, err
|
|
}
|
|
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{
|
|
Accepted: true,
|
|
Action: action,
|
|
Instance: instance,
|
|
Job: job,
|
|
}), nil
|
|
}
|
|
|
|
func (svc *CoreService) lifecycleDependencies(pluginID string, runEndpointID string) (domain.GamePlugin, domain.RunEndpoint, error) {
|
|
plugin, err := svc.store.GamePlugins().Get(pluginID)
|
|
if err != nil {
|
|
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get plugin dependency: %w", err)
|
|
}
|
|
endpoint, err := svc.GetRunEndpoint(runEndpointID)
|
|
if err != nil {
|
|
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
|
}
|
|
return plugin, endpoint, nil
|
|
}
|
|
|
|
func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, action domain.ServerLifecycleAction, idempotencyKey string) (domain.Job, error) {
|
|
capability := domain.LifecycleCapabilityForAction(action)
|
|
binding, err := svc.runtimeBindingForServer(instance.ID)
|
|
if err != nil && !errors.Is(err, repo.ErrNotFound) {
|
|
return domain.Job{}, err
|
|
}
|
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
|
if err != nil {
|
|
return domain.Job{}, err
|
|
}
|
|
profileKey := ""
|
|
if err == nil && strings.TrimSpace(binding.ProfileKey) != "" {
|
|
profileKey = binding.ProfileKey
|
|
} else {
|
|
profileKey = instance.Deployment.ProfileKey
|
|
}
|
|
actionRef := ""
|
|
profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, profileKey)
|
|
if hasProfile {
|
|
if ref := runtimeProfileActionRef(profile.ActionRefs, action); ref != "" {
|
|
actionRef = ref
|
|
}
|
|
}
|
|
if strings.TrimSpace(actionRef) == "" {
|
|
actionRef = lifecycleActionRef(plugin, action)
|
|
}
|
|
if strings.TrimSpace(actionRef) == "" {
|
|
return domain.Job{}, validationError(fmt.Sprintf("plugin %s lifecycle action is required", action))
|
|
}
|
|
var dllExtensions []domain.RuntimeDLLExtensionPlan
|
|
var logSources []domain.RuntimeLogSource
|
|
if action == domain.ServerLifecycleActionStart && hasProfile {
|
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
|
if err != nil {
|
|
return domain.Job{}, err
|
|
}
|
|
dllExtensions, err = lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
|
if err != nil {
|
|
return domain.Job{}, err
|
|
}
|
|
logSources = lifecycleProcessLogSources(plugin.RuntimeProfiles)
|
|
}
|
|
job, err := svc.CreateJob(domain.Job{
|
|
ID: lifecycleJobID(instance.ID, action, idempotencyKey),
|
|
ServerInstanceID: instance.ID,
|
|
RunEndpointID: instance.RunEndpointID,
|
|
Capability: capability,
|
|
TargetKey: actionRef,
|
|
IdempotencyKey: idempotencyKey,
|
|
Progress: lifecycleJobProgress(instance.Deployment),
|
|
ExecutionInput: domain.JobExecutionInput{
|
|
WorkspaceScope: profileKey,
|
|
PluginID: plugin.ID,
|
|
LifecycleOperation: lifecycleExecutionOperation(action),
|
|
LogSources: logSources,
|
|
DLLExtensions: dllExtensions,
|
|
Deployment: deploymentPlanForDispatch(instance.Deployment),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return domain.Job{}, err
|
|
}
|
|
if job.ServerInstanceID != instance.ID || job.RunEndpointID != instance.RunEndpointID || job.Capability != capability {
|
|
return domain.Job{}, validationError("idempotencyKey is already used for a different lifecycle target")
|
|
}
|
|
return job, nil
|
|
}
|
|
|
|
func lifecycleProcessLogSources(profiles domain.GamePluginRuntimeProfiles) []domain.RuntimeLogSource {
|
|
sources := []domain.RuntimeLogSource{}
|
|
for _, source := range profiles.LogSources {
|
|
if source.Kind == "process.stdout" || source.Kind == "process.stderr" {
|
|
sources = append(sources, source)
|
|
}
|
|
}
|
|
return sources
|
|
}
|
|
|
|
func deploymentBootstrapLifecycleAction(plugin domain.GamePlugin, instance domain.ServerInstance) domain.ServerLifecycleAction {
|
|
if instance.Deployment.Mode != domain.ServerDeploymentModeGuided {
|
|
return domain.ServerLifecycleActionCreate
|
|
}
|
|
profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, instance.Deployment.ProfileKey)
|
|
if !exists || !containsString(profile.Capabilities, domain.LifecycleCapabilityStart) {
|
|
return domain.ServerLifecycleActionCreate
|
|
}
|
|
if strings.TrimSpace(runtimeProfileActionRef(profile.ActionRefs, domain.ServerLifecycleActionStart)) == "" && strings.TrimSpace(plugin.LifecycleActions.Start) == "" {
|
|
return domain.ServerLifecycleActionCreate
|
|
}
|
|
return domain.ServerLifecycleActionStart
|
|
}
|
|
|
|
func lifecycleJobProgress(deployment domain.ServerDeploymentDefinition) domain.JobProgress {
|
|
if deployment.Mode != "" {
|
|
return domain.JobProgress{Percent: 0, Phase: "queued", Message: "deployment queued; awaiting Run claim"}
|
|
}
|
|
return domain.JobProgress{}
|
|
}
|
|
|
|
func deploymentPlanForDispatch(definition domain.ServerDeploymentDefinition) *domain.ServerDeploymentDefinition {
|
|
if definition.Mode == "" {
|
|
return nil
|
|
}
|
|
copy := domain.CopyServerDeploymentDefinition(definition)
|
|
return ©
|
|
}
|
|
|
|
func lifecycleExecutionOperation(action domain.ServerLifecycleAction) string {
|
|
switch action {
|
|
case domain.ServerLifecycleActionCreate:
|
|
return "install"
|
|
case domain.ServerLifecycleActionStart:
|
|
return "start"
|
|
case domain.ServerLifecycleActionStop:
|
|
return "stop"
|
|
case domain.ServerLifecycleActionStatus:
|
|
return "status"
|
|
default:
|
|
return string(action)
|
|
}
|
|
}
|
|
|
|
func runtimeProfileActionRef(actions domain.PluginLifecycleActions, action domain.ServerLifecycleAction) string {
|
|
switch action {
|
|
case domain.ServerLifecycleActionCreate:
|
|
return actions.Install
|
|
case domain.ServerLifecycleActionStart:
|
|
return actions.Start
|
|
case domain.ServerLifecycleActionStop:
|
|
return actions.Stop
|
|
case domain.ServerLifecycleActionStatus:
|
|
return actions.Status
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func runtimeLifecycleProfileForKey(profiles domain.GamePluginRuntimeProfiles, key string) (domain.RuntimeLifecycleProfile, bool) {
|
|
return runtimeLifecycleProfile(profiles, key)
|
|
}
|
|
|
|
func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile, endpoint domain.RunEndpoint) ([]domain.RuntimeDLLExtensionPlan, error) {
|
|
if len(profile.DLLExtensionRefs) == 0 {
|
|
return nil, nil
|
|
}
|
|
byKey := make(map[string]domain.RuntimeDLLExtensionProfile, len(profiles.DLLExtensions))
|
|
for _, extension := range profiles.DLLExtensions {
|
|
byKey[extension.Key] = extension
|
|
}
|
|
plans := make([]domain.RuntimeDLLExtensionPlan, 0, len(profile.DLLExtensionRefs))
|
|
for _, key := range profile.DLLExtensionRefs {
|
|
extension, exists := byKey[key]
|
|
if !exists || extension.ReleaseState != "ready" {
|
|
return nil, validationError("extension_release_unavailable: selected DLL release is not ready")
|
|
}
|
|
if !runtimeDLLExtensionSupportsTarget(extension, endpoint.Platform, endpoint.Architecture) {
|
|
return nil, validationError("unsupported_extension_platform: UE4SS DLL requires windows/amd64")
|
|
}
|
|
plans = append(plans, domain.RuntimeDLLExtensionPlan{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort})
|
|
}
|
|
return plans, nil
|
|
}
|
|
|
|
func runtimeDLLExtensionSupportsTarget(extension domain.RuntimeDLLExtensionProfile, platform string, architecture string) bool {
|
|
for _, target := range extension.SupportedTargets {
|
|
if strings.EqualFold(target.OS, platform) && strings.EqualFold(target.Arch, architecture) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (svc *CoreService) validateLifecycleIdempotency(runEndpointID string, idempotencyKey string, serverInstanceID string, capability string) error {
|
|
existing, err := svc.store.Jobs().GetByIdempotency(runEndpointID, idempotencyKey)
|
|
if errors.Is(err, repo.ErrNotFound) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existing.ServerInstanceID == serverInstanceID && existing.Capability == capability {
|
|
return nil
|
|
}
|
|
return validationError("idempotencyKey is already used for a different lifecycle target")
|
|
}
|
|
|
|
func validateLifecycleActionRef(plugin domain.GamePlugin, action domain.ServerLifecycleAction) error {
|
|
if strings.TrimSpace(lifecycleActionRef(plugin, action)) == "" {
|
|
return validationError(fmt.Sprintf("plugin %s lifecycle action is required", action))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lifecycleActionRef(plugin domain.GamePlugin, action domain.ServerLifecycleAction) string {
|
|
switch action {
|
|
case domain.ServerLifecycleActionCreate:
|
|
return plugin.LifecycleActions.Install
|
|
case domain.ServerLifecycleActionStart:
|
|
return plugin.LifecycleActions.Start
|
|
case domain.ServerLifecycleActionStop:
|
|
return plugin.LifecycleActions.Stop
|
|
case domain.ServerLifecycleActionStatus:
|
|
return plugin.LifecycleActions.Status
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func serverStateAllowed(state domain.ServerInstanceState, allowed []domain.ServerInstanceState) bool {
|
|
for _, candidate := range allowed {
|
|
if state == candidate {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func lifecycleJobID(serverInstanceID string, action domain.ServerLifecycleAction, idempotencyKey string) string {
|
|
sum := sha256.Sum256([]byte(idempotencyKey))
|
|
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 == domain.ServerDeploymentModeCustom {
|
|
required = append(required, domain.JobCapabilityDeploymentPlan)
|
|
}
|
|
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
|
|
}
|