240 lines
10 KiB
Go
240 lines
10 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")
|
|
}
|
|
expectedPrerequisites := map[string]string{"steamcmd": "steamcmd", "vcredist-2012-x86": "windows-vcredist", "vcredist-2012-x64": "windows-vcredist", "vcredist-2013-x86": "windows-vcredist", "vcredist-2013-x64": "windows-vcredist", "vcredist-2015-2022-x86": "windows-vcredist", "vcredist-2015-2022-x64": "windows-vcredist", "directx-jun2010": "windows-directx"}
|
|
if len(profile.Prerequisites) != len(expectedPrerequisites) {
|
|
return nil, validationError("SCUM deployment template prerequisite list is incomplete")
|
|
}
|
|
for _, prerequisite := range profile.Prerequisites {
|
|
if expectedPrerequisites[prerequisite.Key] != prerequisite.Kind {
|
|
return nil, validationError("SCUM deployment template prerequisite is invalid")
|
|
}
|
|
}
|
|
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,
|
|
Prerequisites: append([]domain.RuntimeServerPrerequisite(nil), profile.Prerequisites...),
|
|
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 deploymentNeedsCompleteRuntimeBinding(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) bool {
|
|
return !scumDeploymentCapabilityRequired(plugin, definition)
|
|
}
|
|
|
|
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)
|
|
}
|