Files
browser/platform/service/runtime_bindings.go
T

240 lines
8.7 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.ServerInstanceStateDeleted {
return domain.RuntimeBindingView{}, validationError("runtime binding cannot be changed after the server is deleted")
}
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
}
}
// Older persisted bindings used "local" for the only local-process profile.
// Infer that legacy key only when the plugin has one unambiguous candidate.
if key != "local" {
return domain.RuntimeLifecycleProfile{}, false
}
var candidate domain.RuntimeLifecycleProfile
count := 0
for _, profile := range profiles.LifecycleProfiles {
if profile.Mode != "local-process" {
continue
}
candidate = profile
count++
}
if count == 1 {
return candidate, 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, false)
}
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)
}
}
}
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, ok := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey)
if !ok {
return domain.RuntimeBinding{}, validationError("runtime profile is no longer declared")
}
binding.ProfileKey = profile.Key
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
}