Files
browser/platform/service/scum_deployment.go
T

226 lines
9.3 KiB
Go

package service
import (
"fmt"
"regexp"
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/validator"
)
const scumPluginID = "game.scum"
func applyPluginCreateDefaults(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) domain.ServerDeploymentDefinition {
definition = domain.CopyServerDeploymentDefinition(definition)
if definition.Mode != domain.ServerDeploymentModeGuided {
return definition
}
if definition.CreateInputs == nil {
definition.CreateInputs = map[string]string{}
}
for _, field := range plugin.CreateFields {
if _, present := definition.CreateInputs[field.Key]; !present && field.DefaultValue != "" {
definition.CreateInputs[field.Key] = field.DefaultValue
}
}
return definition
}
func scumDeploymentPlan(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition, operation string) (*domain.ServerDeploymentPlan, error) {
if plugin.ID != scumPluginID || (definition.Mode != domain.ServerDeploymentModeGuided && definition.Mode != domain.ServerDeploymentModeExisting) {
return nil, nil
}
if operation != "install" && operation != "adopt" {
return nil, validationError("SCUM deployment operation is invalid")
}
if len(plugin.RuntimeProfiles.ServerDeployments) == 0 {
return nil, validationError("SCUM deployment template is not registered")
}
profile := plugin.RuntimeProfiles.ServerDeployments[0]
if strings.TrimSpace(profile.Key) == "" || strings.TrimSpace(profile.Version) == "" || profile.SteamAppID == "" {
return nil, validationError("SCUM deployment template is incomplete")
}
fieldKeys := make(map[string]struct{}, len(plugin.CreateFields))
for _, field := range plugin.CreateFields {
fieldKeys[field.Key] = struct{}{}
}
for _, mapping := range profile.ConfigMappings {
if _, ok := fieldKeys[mapping.FieldKey]; !ok {
return nil, validationError("SCUM deployment template maps an undeclared create field")
}
if strings.TrimSpace(mapping.ConfigKey) == "" || strings.TrimSpace(mapping.ValueType) == "" {
return nil, validationError("SCUM deployment template contains an incomplete config mapping")
}
}
for _, check := range profile.VerificationChecks {
if check.Required && (strings.TrimSpace(check.Key) == "" || strings.TrimSpace(check.Kind) == "") {
return nil, validationError("SCUM deployment template contains an incomplete verification check")
}
}
return &domain.ServerDeploymentPlan{
SchemaVersion: "1",
Operation: operation,
PluginID: plugin.ID,
TemplateKey: profile.Key,
TemplateVersion: profile.Version,
SteamAppID: profile.SteamAppID,
ExecutableKey: profile.ExecutableKey,
InstallRootKey: profile.InstallRootKey,
ConfigKey: profile.ConfigKey,
ConfigFormat: profile.ConfigFormat,
ConfigMappings: append([]domain.RuntimeServerConfigMapping(nil), profile.ConfigMappings...),
DiscoveryMarkers: append([]domain.RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...),
VerificationChecks: append([]domain.RuntimeServerVerificationCheck(nil), profile.VerificationChecks...),
}, nil
}
func scumDeploymentProjection(plan *domain.ServerDeploymentPlan, operation string, stamp time.Time) domain.ServerDeploymentProjection {
projection := domain.ServerDeploymentProjection{State: "draft", Operation: operation, UpdatedAt: stamp}
if plan != nil {
projection.TemplateKey = plan.TemplateKey
projection.TemplateVersion = plan.TemplateVersion
}
return projection
}
func scumDeploymentCapabilityRequired(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) bool {
return plugin.ID == scumPluginID && (definition.Mode == domain.ServerDeploymentModeGuided || definition.Mode == domain.ServerDeploymentModeExisting)
}
func validateSCUMDeploymentTarget(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition, endpoint domain.RunEndpoint) error {
if !scumDeploymentCapabilityRequired(plugin, definition) {
return nil
}
plan, err := scumDeploymentPlan(plugin, definition, "install")
if err != nil {
return err
}
for _, profile := range plugin.RuntimeProfiles.ServerDeployments {
if profile.Key != plan.TemplateKey {
continue
}
for _, target := range profile.SupportedTargets {
if target.OS == endpoint.Platform && target.Arch == endpoint.Architecture {
return nil
}
}
}
return validationError("SCUM deployment template is incompatible with the selected Run target")
}
func validateScumDeploymentInputs(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) error {
plan, err := scumDeploymentPlan(plugin, definition, "install")
if err != nil || plan == nil {
return err
}
if definition.Mode == domain.ServerDeploymentModeExisting {
if strings.TrimSpace(definition.ServerRoot) == "" {
return validationError("SCUM adoption requires an existing server directory")
}
return nil
}
if strings.TrimSpace(definition.ServerRoot) == "" {
return validationError("SCUM installation requires an install directory")
}
for _, mapping := range plan.ConfigMappings {
if mapping.Required && strings.TrimSpace(definition.CreateInputs[mapping.FieldKey]) == "" {
field, found := findPluginCreateField(plugin.CreateFields, mapping.FieldKey)
if !found || field.DefaultValue == "" {
return validator.ValidationError{Violations: []string{"createInputs." + mapping.FieldKey + " is required by the SCUM deployment template"}}
}
}
}
return nil
}
func findPluginCreateField(fields []domain.PluginCreateField, key string) (domain.PluginCreateField, bool) {
for _, field := range fields {
if field.Key == key {
return field, true
}
}
return domain.PluginCreateField{}, false
}
func validateSCUMDeploymentEvidence(plan *domain.ServerDeploymentPlan, result domain.RunJobResult) error {
if plan == nil {
return nil
}
evidence := result.ExecutionResult.ServerDeploymentEvidence
if evidence == nil {
return validationError("SCUM deployment result must include deployment evidence")
}
if evidence.TemplateKey != plan.TemplateKey || evidence.TemplateVersion != plan.TemplateVersion {
return validationError("SCUM deployment result template fence is invalid")
}
for name, values := range map[string]map[string]string{"discoveredFacts": evidence.DiscoveredFacts, "mappingResults": evidence.MappingResults, "verificationResults": evidence.VerificationResults} {
if len(values) > 64 {
return validationError("SCUM deployment evidence contains too many " + name)
}
for key, value := range values {
if !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]{0,119}$`).MatchString(key) || len(value) > 160 || strings.TrimSpace(value) != value || unsafeSCUMEvidenceValue(value) {
return validationError(fmt.Sprintf("SCUM deployment evidence contains an unsafe %s value", name))
}
}
}
if len(evidence.FailureCode) > 80 || (evidence.FailureCode != "" && !regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,79}$`).MatchString(evidence.FailureCode)) {
return validationError("SCUM deployment failure code is invalid")
}
if result.State == domain.JobStateFailed {
if result.ExecutionResult.Kind != "scum.install.failed" && result.ExecutionResult.Kind != "scum.adopt.failed" {
return validationError("SCUM deployment failure result type is invalid")
}
if strings.TrimSpace(evidence.FailureCode) == "" {
return validationError("SCUM deployment failure must include a stable failure code")
}
return nil
}
if result.State != domain.JobStateSucceeded {
return nil
}
expectedKind := "scum.install.completed"
if plan.Operation == "adopt" {
expectedKind = "scum.adopt.completed"
}
if result.ExecutionResult.Kind != expectedKind {
return validationError("SCUM deployment result type is invalid")
}
if evidence.PreflightState != "passed" || evidence.DiscoveryState != "passed" || evidence.VerificationState != "passed" {
return validationError("SCUM deployment result is missing successful preflight, discovery, or verification evidence")
}
validMappingState := evidence.MappingState == "passed" || evidence.MappingState == "applied" || evidence.MappingState == "unchanged"
if plan.Operation == "adopt" && evidence.MappingState == "skipped" {
validMappingState = true
}
if !validMappingState {
return validationError("SCUM deployment result is missing a successful config mapping state")
}
for _, mapping := range plan.ConfigMappings {
mappingResult := evidence.MappingResults[mapping.FieldKey]
if plan.Operation == "adopt" && mappingResult == "" {
mappingResult = "skipped"
}
if mapping.Required && mappingResult != "applied" && mappingResult != "unchanged" && !(plan.Operation == "adopt" && mappingResult == "skipped") {
return validationError("SCUM deployment result is missing a required config mapping")
}
}
for _, check := range plan.VerificationChecks {
if check.Required && evidence.VerificationResults[check.Key] != "passed" {
return validationError("SCUM deployment result is missing a required verification check")
}
}
return nil
}
func unsafeSCUMEvidenceValue(value string) bool {
lower := strings.ToLower(value)
for _, token := range []string{"password", "secret", "credential", "token", "private key", "powershell", "cmd.exe", "bash -c", "ssh://", "tcp://", "udp://"} {
if strings.Contains(lower, token) {
return true
}
}
return strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\\`) || regexp.MustCompile(`^[A-Za-z]:[\\/]`).MatchString(value)
}