feat: support custom server deployment drafts

This commit is contained in:
npc0-hue
2026-07-24 16:56:28 +08:00
parent 292b380f3c
commit 220ef91a8e
36 changed files with 1520 additions and 85 deletions
+74 -11
View File
@@ -18,13 +18,21 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
return domain.ServerLifecycleResult{}, err
}
plugin, endpoint, err := svc.lifecycleDependencies(create.PluginID, create.RunEndpointID)
plugin, err := svc.store.GamePlugins().Get(create.PluginID)
if err != nil {
return domain.ServerLifecycleResult{}, err
return domain.ServerLifecycleResult{}, fmt.Errorf("get plugin dependency: %w", err)
}
if plugin.Status != domain.GamePluginStatusInstalled {
return domain.ServerLifecycleResult{}, validationError("plugin must be installed")
}
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{
@@ -39,31 +47,63 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
ConfigKey: "server.properties",
CreatedAt: stamp,
UpdatedAt: stamp,
Deployment: create.Deployment,
}
if instance.Deployment.Mode != "" {
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.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.store.RunEndpoints().Get(create.RunEndpointID)
if err != nil {
return domain.ServerLifecycleResult{}, fmt.Errorf("get run endpoint dependency: %w", err)
}
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
return domain.ServerLifecycleResult{}, err
}
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
return domain.ServerLifecycleResult{}, err
}
if instance.Deployment.Mode != "" && !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 := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
return domain.ServerLifecycleResult{}, err
}
binding, err := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
if err != nil {
return domain.ServerLifecycleResult{}, err
var binding domain.RuntimeBinding
hasBinding := strings.TrimSpace(create.ProfileKey) != ""
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 err := svc.store.RuntimeBindings().Create(binding); 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, domain.ServerLifecycleActionCreate, create.IdempotencyKey)
@@ -208,15 +248,21 @@ func (svc *CoreService) lifecycleDependencies(pluginID string, runEndpointID str
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 {
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
}
actionRef := binding.ProfileKey
profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey)
profileKey := ""
if err == nil {
profileKey = binding.ProfileKey
} else {
profileKey = instance.Deployment.ProfileKey
}
actionRef := profileKey
profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, profileKey)
if hasProfile {
if ref := runtimeProfileActionRef(profile.ActionRefs, action); ref != "" {
actionRef = ref
@@ -245,11 +291,13 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
Capability: capability,
TargetKey: actionRef,
IdempotencyKey: idempotencyKey,
Progress: lifecycleJobProgress(instance.Deployment),
ExecutionInput: domain.JobExecutionInput{
WorkspaceScope: binding.ProfileKey,
WorkspaceScope: profileKey,
PluginID: plugin.ID,
LifecycleOperation: lifecycleExecutionOperation(action),
DLLExtensions: dllExtensions,
Deployment: deploymentPlanForDispatch(instance.Deployment),
},
})
if err != nil {
@@ -261,6 +309,21 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
return job, nil
}
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 &copy
}
func lifecycleExecutionOperation(action domain.ServerLifecycleAction) string {
switch action {
case domain.ServerLifecycleActionCreate: