225 lines
8.4 KiB
Go
225 lines
8.4 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/repo"
|
|
"browser.local/platform/validator"
|
|
)
|
|
|
|
func (svc *CoreService) GetServerRuntimeBindingForSession(sessionID, serverInstanceID string) (domain.RuntimeBindingView, error) {
|
|
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
binding, err := svc.runtimeBindingForServer(instance.ID)
|
|
if errors.Is(err, repo.ErrNotFound) {
|
|
return domain.RuntimeBindingView{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Status: domain.RuntimeBindingStatusIncomplete, Reason: "runtime profile is not configured"}, nil
|
|
}
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
return runtimeBindingView(plugin, binding)
|
|
}
|
|
|
|
func (svc *CoreService) UpdateServerRuntimeBindingForSession(sessionID, serverInstanceID string, update domain.RuntimeBindingUpdate) (domain.RuntimeBindingView, error) {
|
|
_, instance, err := svc.requireServerOwner(sessionID, serverInstanceID)
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
existing, existingErr := svc.runtimeBindingForServer(instance.ID)
|
|
if existingErr != nil && !errors.Is(existingErr, repo.ErrNotFound) {
|
|
return domain.RuntimeBindingView{}, existingErr
|
|
}
|
|
if (instance.State == domain.ServerInstanceStateInstalling || instance.State == domain.ServerInstanceStateRunning) && existingErr == nil || instance.State == domain.ServerInstanceStateDeleted {
|
|
return domain.RuntimeBindingView{}, validationError("runtime binding cannot be changed while the server is active")
|
|
}
|
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
binding, err := svc.buildRuntimeBinding(instance, plugin, update, false)
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
if existingErr == nil {
|
|
binding.CreatedAt = existing.CreatedAt
|
|
if existing.ProfileKey == binding.ProfileKey {
|
|
merged := domain.CopyStringMap(existing.Bindings)
|
|
for key, value := range update.Bindings {
|
|
if strings.TrimSpace(value) == "" {
|
|
delete(merged, key)
|
|
} else {
|
|
merged[key] = value
|
|
}
|
|
}
|
|
binding, err = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: update.ProfileKey, Bindings: merged}, false)
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
binding.CreatedAt = existing.CreatedAt
|
|
}
|
|
if err := svc.store.RuntimeBindings().Update(binding); err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
} else if errors.Is(existingErr, repo.ErrNotFound) {
|
|
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
}
|
|
return runtimeBindingView(plugin, binding)
|
|
}
|
|
|
|
func (svc *CoreService) buildRuntimeBinding(instance domain.ServerInstance, plugin domain.GamePlugin, update domain.RuntimeBindingUpdate, requireComplete bool) (domain.RuntimeBinding, error) {
|
|
update = domain.CopyRuntimeBindingUpdate(update)
|
|
profile, ok := runtimeLifecycleProfile(plugin.RuntimeProfiles, update.ProfileKey)
|
|
if !ok {
|
|
return domain.RuntimeBinding{}, validationError("profileKey must reference a declared lifecycle profile")
|
|
}
|
|
stamp := svc.now()
|
|
binding := domain.RuntimeBinding{ID: "runtime-binding-" + instance.ID, ServerInstanceID: instance.ID, PluginID: plugin.ID, PluginVersion: plugin.Version, ProfileKey: profile.Key, Mode: profile.Mode, Bindings: update.Bindings, CreatedAt: stamp, UpdatedAt: stamp}
|
|
binding, err := normalizeRuntimeBinding(plugin, binding)
|
|
if err != nil {
|
|
return domain.RuntimeBinding{}, err
|
|
}
|
|
if requireComplete && binding.Status != domain.RuntimeBindingStatusComplete {
|
|
return domain.RuntimeBinding{}, validationError("missing runtime bindings: " + strings.Join(binding.MissingKeys, ", "))
|
|
}
|
|
return binding, nil
|
|
}
|
|
|
|
func runtimeLifecycleProfile(profiles domain.GamePluginRuntimeProfiles, key string) (domain.RuntimeLifecycleProfile, bool) {
|
|
for _, profile := range profiles.LifecycleProfiles {
|
|
if profile.Key == key {
|
|
return profile, true
|
|
}
|
|
}
|
|
return domain.RuntimeLifecycleProfile{}, false
|
|
}
|
|
|
|
func runtimeBindingKeys(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile) ([]string, map[string]struct{}) {
|
|
requiredSet := map[string]struct{}{}
|
|
allowed := map[string]struct{}{}
|
|
add := func(key string, required bool) {
|
|
if key == "" {
|
|
return
|
|
}
|
|
allowed[key] = struct{}{}
|
|
if required {
|
|
requiredSet[key] = struct{}{}
|
|
}
|
|
}
|
|
for _, probe := range profiles.Discovery {
|
|
add(probe.TargetKey, probe.Required)
|
|
}
|
|
for _, probe := range profiles.DependencyProbes {
|
|
add(probe.TargetKey, probe.Required)
|
|
}
|
|
for _, source := range profiles.LogSources {
|
|
add(source.TargetKey, source.TargetKey != "")
|
|
}
|
|
for _, plan := range profiles.InstallPlans {
|
|
for _, step := range plan.Steps {
|
|
add(step.TargetKey, false)
|
|
}
|
|
}
|
|
for _, transport := range profiles.TransportProfiles {
|
|
if containsString(profile.TransportKeys, transport.Key) {
|
|
if transport.TargetKey != "" {
|
|
add(transport.TargetKey, true)
|
|
} else {
|
|
add(transport.Key, true)
|
|
}
|
|
}
|
|
}
|
|
if profile.ClientManagerRef != "" {
|
|
add(profile.ClientManagerRef, true)
|
|
}
|
|
required := make([]string, 0, len(requiredSet))
|
|
for key := range requiredSet {
|
|
required = append(required, key)
|
|
}
|
|
sort.Strings(required)
|
|
return required, allowed
|
|
}
|
|
|
|
func normalizeRuntimeBinding(plugin domain.GamePlugin, binding domain.RuntimeBinding) (domain.RuntimeBinding, error) {
|
|
profile, _ := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey)
|
|
if profile.Key == "" {
|
|
return domain.RuntimeBinding{}, validationError("runtime profile is no longer declared")
|
|
}
|
|
required, allowed := runtimeBindingKeys(plugin.RuntimeProfiles, profile)
|
|
for key := range binding.Bindings {
|
|
if _, ok := allowed[key]; !ok {
|
|
return domain.RuntimeBinding{}, validationError(fmt.Sprintf("bindings.%s is not declared by runtime profile", key))
|
|
}
|
|
}
|
|
missing := make([]string, 0)
|
|
for _, key := range required {
|
|
if strings.TrimSpace(binding.Bindings[key]) == "" {
|
|
missing = append(missing, key)
|
|
}
|
|
}
|
|
binding.Mode = profile.Mode
|
|
binding.MissingKeys = missing
|
|
binding.Status = domain.RuntimeBindingStatusComplete
|
|
if len(missing) > 0 {
|
|
binding.Status = domain.RuntimeBindingStatusIncomplete
|
|
}
|
|
if err := validator.ValidateRuntimeBinding(binding); err != nil {
|
|
return domain.RuntimeBinding{}, err
|
|
}
|
|
return binding, nil
|
|
}
|
|
|
|
func runtimeBindingView(plugin domain.GamePlugin, binding domain.RuntimeBinding) (domain.RuntimeBindingView, error) {
|
|
binding, err := normalizeRuntimeBinding(plugin, binding)
|
|
if err != nil {
|
|
return domain.RuntimeBindingView{}, err
|
|
}
|
|
profile, _ := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey)
|
|
required, allowed := runtimeBindingKeys(plugin.RuntimeProfiles, profile)
|
|
requiredSet := map[string]struct{}{}
|
|
for _, key := range required {
|
|
requiredSet[key] = struct{}{}
|
|
}
|
|
keys := make([]string, 0, len(allowed))
|
|
for key := range allowed {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
items := make([]domain.RuntimeBindingKeyView, 0, len(keys))
|
|
for _, key := range keys {
|
|
value := strings.TrimSpace(binding.Bindings[key])
|
|
_, isRequired := requiredSet[key]
|
|
items = append(items, domain.RuntimeBindingKeyView{Key: key, Required: isRequired, Configured: value != "", Secret: strings.HasPrefix(value, "secret://")})
|
|
}
|
|
reason := ""
|
|
if binding.Status != domain.RuntimeBindingStatusComplete {
|
|
reason = "required logical bindings are missing"
|
|
}
|
|
return domain.RuntimeBindingView{ServerInstanceID: binding.ServerInstanceID, PluginID: binding.PluginID, ProfileKey: binding.ProfileKey, Mode: binding.Mode, Configured: true, Keys: items, MissingKeys: domain.CopyStringSlice(binding.MissingKeys), Status: binding.Status, Reason: reason, CreatedAt: binding.CreatedAt, UpdatedAt: binding.UpdatedAt}, nil
|
|
}
|
|
|
|
func (svc *CoreService) runtimeBindingForServer(serverInstanceID string) (domain.RuntimeBinding, error) {
|
|
bindings, err := svc.store.RuntimeBindings().List(domain.RuntimeBindingFilter{ServerInstanceID: serverInstanceID})
|
|
if err != nil {
|
|
return domain.RuntimeBinding{}, err
|
|
}
|
|
if len(bindings) == 0 {
|
|
return domain.RuntimeBinding{}, repo.ErrNotFound
|
|
}
|
|
if len(bindings) > 1 {
|
|
return domain.RuntimeBinding{}, validationError("server has multiple runtime bindings")
|
|
}
|
|
return bindings[0], nil
|
|
}
|