Files
run/runtime/runtime_profiles.go
T
2026-08-26 09:56:43 +08:00

298 lines
9.9 KiB
Go

package runtime
import (
"fmt"
"sort"
"strings"
"browser.local/run/protocol"
)
const (
RuntimeModeLocalProcess = "local-process"
RuntimeModeHostedFTPRCON = "hosted-ftp-rcon"
RuntimeModeFTPOnly = "ftp-only"
RuntimeModeCustomClient = "custom-client"
)
type RuntimeProfiles struct {
Discovery []RuntimeDiscoveryProbe `json:"discovery,omitempty"`
LifecycleProfiles []RuntimeLifecycleProfile `json:"lifecycleProfiles,omitempty"`
DependencyProbes []RuntimeDependencyProbe `json:"dependencyProbes,omitempty"`
InstallPlans []RuntimeInstallPlan `json:"installPlans,omitempty"`
LogSources []RuntimeLogSource `json:"logSources,omitempty"`
TransportProfiles []RuntimeTransportProfile `json:"transportProfiles,omitempty"`
ClientManagers []RuntimeClientManagerSpec `json:"clientManagers,omitempty"`
}
type RuntimeDiscoveryProbe struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey"`
Required bool `json:"required,omitempty"`
Platforms []string `json:"platforms,omitempty"`
}
type RuntimeLifecycleProfile struct {
Key string `json:"key"`
Mode string `json:"mode"`
Capabilities []string `json:"capabilities"`
ActionRefs map[string]string `json:"actionRefs,omitempty"`
TransportKeys []string `json:"transportKeys,omitempty"`
ClientManagerRef string `json:"clientManagerRef,omitempty"`
Platforms []string `json:"platforms,omitempty"`
}
type RuntimeDependencyProbe struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey"`
Required bool `json:"required,omitempty"`
Platforms []string `json:"platforms,omitempty"`
}
type RuntimeInstallPlan struct {
Key string `json:"key"`
Title string `json:"title"`
Platforms []string `json:"platforms,omitempty"`
Steps []RuntimeInstallStep `json:"steps"`
}
type RuntimeInstallStep struct {
Type string `json:"type"`
TargetKey string `json:"targetKey"`
PackageManager string `json:"packageManager,omitempty"`
PackageName string `json:"packageName,omitempty"`
Version string `json:"version,omitempty"`
DownloadRef string `json:"downloadRef,omitempty"`
Checksum string `json:"checksum,omitempty"`
}
type RuntimeLogSource struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey,omitempty"`
StreamKey string `json:"streamKey"`
CursorKind string `json:"cursorKind,omitempty"`
RetentionDays int `json:"retentionDays,omitempty"`
}
type RuntimeTransportProfile struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey,omitempty"`
Capabilities []string `json:"capabilities"`
}
type RuntimeClientManagerSpec struct {
Key string `json:"key"`
}
type RuntimeBindingSet struct {
ProfileKey string `json:"profileKey"`
Mode string `json:"mode"`
Bindings map[string]string `json:"bindings,omitempty"`
MissingKeys []string `json:"missingKeys,omitempty"`
}
type RuntimeResolution struct {
ProfileKey string `json:"profileKey"`
Mode string `json:"mode"`
Capabilities []string `json:"capabilities"`
ActionRefs map[string]string `json:"actionRefs,omitempty"`
TransportKeys []string `json:"transportKeys,omitempty"`
Transports []RuntimeTransportProfile `json:"transports,omitempty"`
LogSources []RuntimeLogSource `json:"logSources,omitempty"`
Discovery []RuntimeDiscoveryProbe `json:"discovery,omitempty"`
ClientManagerRef string `json:"clientManagerRef,omitempty"`
MissingKeys []string `json:"missingKeys,omitempty"`
Available bool `json:"available"`
}
func ResolveRuntimeProfile(profiles RuntimeProfiles, profileKey string, targetOS string, binding RuntimeBindingSet) (RuntimeResolution, error) {
profile, ok := findLifecycleProfile(profiles.LifecycleProfiles, profileKey)
if !ok {
return RuntimeResolution{}, fmt.Errorf("runtime profile is not declared")
}
if !supportedRuntimeMode(profile.Mode) {
return RuntimeResolution{}, fmt.Errorf("runtime mode is unsupported")
}
if targetOS != "" && !supportsPlatform(profile.Platforms, targetOS) {
return RuntimeResolution{}, fmt.Errorf("runtime profile does not support target platform")
}
if binding.ProfileKey != "" && binding.ProfileKey != profile.Key {
return RuntimeResolution{}, fmt.Errorf("runtime binding profile does not match")
}
if binding.Mode != "" && binding.Mode != profile.Mode {
return RuntimeResolution{}, fmt.Errorf("runtime binding mode does not match")
}
if err := validateRuntimeProfile(profile); err != nil {
return RuntimeResolution{}, err
}
transports, err := resolveTransports(profile.TransportKeys, profiles.TransportProfiles)
if err != nil {
return RuntimeResolution{}, err
}
missing := missingRuntimeBindingKeys(profile, transports, profiles.Discovery, profiles.LogSources, binding)
return RuntimeResolution{
ProfileKey: profile.Key,
Mode: profile.Mode,
Capabilities: append([]string(nil), profile.Capabilities...),
ActionRefs: copyStringMap(profile.ActionRefs),
TransportKeys: append([]string(nil), profile.TransportKeys...),
Transports: transports,
LogSources: safeLogSources(profiles.LogSources, targetOS),
Discovery: safeDiscovery(profiles.Discovery, targetOS),
ClientManagerRef: profile.ClientManagerRef,
MissingKeys: missing,
Available: len(missing) == 0,
}, nil
}
func findLifecycleProfile(profiles []RuntimeLifecycleProfile, key string) (RuntimeLifecycleProfile, bool) {
for _, profile := range profiles {
if profile.Key == key {
return profile, true
}
}
return RuntimeLifecycleProfile{}, false
}
func validateRuntimeProfile(profile RuntimeLifecycleProfile) error {
if !protocol.ValidLogicalFileKey(profile.Key) {
return fmt.Errorf("runtime profile key is unsafe")
}
for _, capability := range profile.Capabilities {
if strings.TrimSpace(capability) == "" || containsUnsafeRuntimeText(capability) {
return fmt.Errorf("runtime capability is unsafe")
}
}
for action, ref := range profile.ActionRefs {
if !protocol.ValidLogicalFileKey(action) || !protocol.ValidLogicalFileKey(ref) {
return fmt.Errorf("runtime action ref is unsafe")
}
}
if profile.ClientManagerRef != "" && !protocol.ValidLogicalFileKey(profile.ClientManagerRef) {
return fmt.Errorf("client manager ref is unsafe")
}
return nil
}
func resolveTransports(keys []string, profiles []RuntimeTransportProfile) ([]RuntimeTransportProfile, error) {
out := make([]RuntimeTransportProfile, 0, len(keys))
for _, key := range keys {
if !protocol.ValidLogicalFileKey(key) {
return nil, fmt.Errorf("transport key is unsafe")
}
found := false
for _, profile := range profiles {
if profile.Key != key {
continue
}
if !protocol.ValidLogicalFileKey(profile.Key) || (profile.TargetKey != "" && !protocol.ValidLogicalFileKey(profile.TargetKey)) {
return nil, fmt.Errorf("transport profile is unsafe")
}
out = append(out, profile)
found = true
break
}
if !found {
return nil, fmt.Errorf("transport profile %q is not declared", key)
}
}
return out, nil
}
func missingRuntimeBindingKeys(profile RuntimeLifecycleProfile, transports []RuntimeTransportProfile, discovery []RuntimeDiscoveryProbe, logs []RuntimeLogSource, binding RuntimeBindingSet) []string {
required := map[string]struct{}{}
for _, transport := range transports {
if transport.TargetKey != "" {
required[transport.TargetKey] = struct{}{}
}
}
for _, probe := range discovery {
if probe.Required && probe.TargetKey != "" {
required[probe.TargetKey] = struct{}{}
}
}
logTargets := map[string]struct{}{}
for _, source := range logs {
if source.TargetKey != "" {
logTargets[source.TargetKey] = struct{}{}
}
}
if profile.ClientManagerRef != "" {
required[profile.ClientManagerRef] = struct{}{}
}
for _, key := range binding.MissingKeys {
if _, logTarget := logTargets[key]; logTarget {
continue
}
if protocol.ValidLogicalFileKey(key) {
required[key] = struct{}{}
}
}
missing := make([]string, 0, len(required))
for key := range required {
if _, ok := binding.Bindings[key]; !ok {
missing = append(missing, key)
}
}
sort.Strings(missing)
return missing
}
func safeDiscovery(probes []RuntimeDiscoveryProbe, targetOS string) []RuntimeDiscoveryProbe {
out := []RuntimeDiscoveryProbe{}
for _, probe := range probes {
if supportsPlatform(probe.Platforms, targetOS) && protocol.ValidLogicalFileKey(probe.Key) && protocol.ValidLogicalFileKey(probe.TargetKey) {
out = append(out, probe)
}
}
return out
}
func safeLogSources(sources []RuntimeLogSource, targetOS string) []RuntimeLogSource {
_ = targetOS
out := []RuntimeLogSource{}
for _, source := range sources {
if protocol.ValidLogicalFileKey(source.Key) && protocol.ValidLogicalFileKey(source.StreamKey) && (source.TargetKey == "" || protocol.ValidLogicalFileKey(source.TargetKey)) {
out = append(out, source)
}
}
return out
}
func supportedRuntimeMode(mode string) bool {
switch mode {
case RuntimeModeLocalProcess, RuntimeModeHostedFTPRCON, RuntimeModeFTPOnly, RuntimeModeCustomClient:
return true
default:
return false
}
}
func supportsPlatform(platforms []string, targetOS string) bool {
if targetOS == "" || len(platforms) == 0 {
return true
}
for _, platform := range platforms {
if platform == targetOS {
return true
}
}
return false
}
func copyStringMap(values map[string]string) map[string]string {
if len(values) == 0 {
return nil
}
out := make(map[string]string, len(values))
for key, value := range values {
out[key] = value
}
return out
}