first commit
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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, endpoint, err := svc.lifecycleDependencies(create.PluginID, create.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); 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,
|
||||
CreatedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
}
|
||||
if err := validator.ValidateServerInstance(instance); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
|
||||
job, err := svc.dispatchLifecycleJob(instance, domain.ServerLifecycleActionCreate, 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 (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,
|
||||
})
|
||||
}
|
||||
|
||||
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) 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 := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); 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.store.RunEndpoints().Get(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)
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: lifecycleJobID(instance.ID, action, idempotencyKey),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: capability,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
})
|
||||
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 (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
|
||||
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]))
|
||||
}
|
||||
Reference in New Issue
Block a user