Remove legacy runtime deployment paths
This commit is contained in:
@@ -37,5 +37,5 @@ PLATFORM_BUILDER_SOURCE_REPOSITORY=git@git.npc0.com:admin343/run.git
|
||||
PLATFORM_BUILDER_SOURCE_REVISION=main
|
||||
PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds
|
||||
PLATFORM_BUILDER_TIMEOUT_SECONDS=1800
|
||||
# URL embedded into generated Run and client-manager packages.
|
||||
PLATFORM_RUN_RELEASE_URL=https://scum.npc0.com
|
||||
# URL embedded into generated Run packages; override for tunnel or production access.
|
||||
PLATFORM_RUN_RELEASE_URL=http://127.0.0.1:8080
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ Runtime configuration:
|
||||
- `PLATFORM_BUILDER_WORKSPACE_DIR`: private per-plugin/per-job build workspace, default `<PLATFORM_DATA_DIR>/distribution-builds`.
|
||||
- `PLATFORM_BUILDER_CACHE_DIR`: persistent Go build/module cache, default `<PLATFORM_DATA_DIR>/distribution-build-cache`; it contains no job inputs or component keys.
|
||||
- `PLATFORM_BUILDER_TIMEOUT_SECONDS`: positive build deadline, default `1800`.
|
||||
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated components, default `https://scum.npc0.com/`.
|
||||
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated Run packages, default `http://127.0.0.1:8080/`; set this explicitly for tunnel or production access.
|
||||
|
||||
Build the dedicated toolchain image before enabling distribution generation:
|
||||
|
||||
|
||||
@@ -912,6 +912,10 @@ func (h *coreHandlers) gamePlugins(w http.ResponseWriter, r *http.Request) {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
if violations := request.RuntimeProfiles.UnsupportedLegacyProfileViolations("runtimeProfiles"); len(violations) > 0 {
|
||||
writeServiceError(w, validator.ValidationError{Violations: violations})
|
||||
return
|
||||
}
|
||||
plugin, err := h.core.CreateGamePlugin(request.ToDomain())
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
@@ -944,7 +948,7 @@ func (h *coreHandlers) gamePluginManifestRegistration(w http.ResponseWriter, r *
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
if violations := request.Manifest.RuntimeProfiles.UnsupportedLegacyClientManagerViolations("manifest.runtimeProfiles"); len(violations) > 0 {
|
||||
if violations := request.Manifest.RuntimeProfiles.UnsupportedLegacyProfileViolations("manifest.runtimeProfiles"); len(violations) > 0 {
|
||||
writeServiceError(w, validator.ValidationError{Violations: violations})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,6 +129,24 @@ func TestCoreAPICreateListDetailWorkflows(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestCoreAPIGamePluginCreateRejectsLegacyServerDeployments(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
request := validGamePluginRequest()
|
||||
request.RuntimeProfiles.ServerDeployments = json.RawMessage(`[{"key":"legacy"}]`)
|
||||
|
||||
response := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins", request)
|
||||
assertErrorResponse(t, response, http.StatusBadRequest, errorCodeValidation)
|
||||
}
|
||||
|
||||
func TestCoreAPIGamePluginManifestRejectsLegacyServerDeployments(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
registration := validGamePluginManifestRegistrationRequest()
|
||||
registration.Manifest.RuntimeProfiles.ServerDeployments = json.RawMessage(`[{"key":"legacy"}]`)
|
||||
|
||||
response := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins/register-manifest", registration)
|
||||
assertErrorResponse(t, response, http.StatusBadRequest, errorCodeValidation)
|
||||
}
|
||||
|
||||
func TestMetricsAndConfigReadAPIAreSafeAndRoleScoped(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
@@ -1519,7 +1537,7 @@ func TestGamePluginManifestAPISafelyProjectsDLLReleaseDeclaration(t *testing.T)
|
||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []dto.RuntimeTargetBody{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []dto.RuntimeTargetBody{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
}}}
|
||||
|
||||
recorder := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins/register-manifest", registration)
|
||||
|
||||
@@ -197,17 +197,17 @@ type RunAutonomousLogSource struct {
|
||||
}
|
||||
|
||||
type RunAutonomousDLLExtension struct {
|
||||
Key string `json:"key"`
|
||||
Version string `json:"version"`
|
||||
ReleaseURL string `json:"releaseUrl"`
|
||||
Checksum string `json:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
RCONPort int `json:"rconPort,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Version string `json:"version"`
|
||||
ReleaseURL string `json:"releaseUrl"`
|
||||
Checksum string `json:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
RCONPort int `json:"rconPort,omitempty"`
|
||||
}
|
||||
|
||||
// RunAutonomousDataTarget is a package-local snapshot declaration. The source
|
||||
|
||||
@@ -462,41 +462,41 @@ type RuntimeDataTarget struct {
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionProfile struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
Kind string
|
||||
Activation string
|
||||
Version string
|
||||
ReleaseState string
|
||||
ReleaseURL string
|
||||
Checksum string
|
||||
SizeBytes int64
|
||||
TargetKey string
|
||||
ModKey string
|
||||
DLLRef string
|
||||
SCUMExecutableChecksum string
|
||||
UE4SSABI string
|
||||
SupportedTargets []RuntimeTarget
|
||||
UpdateOnStart bool
|
||||
RCONPort int
|
||||
Key string
|
||||
DisplayName string
|
||||
Kind string
|
||||
Activation string
|
||||
Version string
|
||||
ReleaseState string
|
||||
ReleaseURL string
|
||||
Checksum string
|
||||
SizeBytes int64
|
||||
TargetKey string
|
||||
ModKey string
|
||||
DLLRef string
|
||||
TargetExecutableChecksum string
|
||||
UE4SSABI string
|
||||
SupportedTargets []RuntimeTarget
|
||||
UpdateOnStart bool
|
||||
RCONPort int
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionPlan struct {
|
||||
Key string
|
||||
Version string
|
||||
ReleaseURL string
|
||||
Checksum string
|
||||
SizeBytes int64
|
||||
TargetKey string
|
||||
ModKey string
|
||||
DLLRef string
|
||||
SCUMExecutableChecksum string
|
||||
UE4SSABI string
|
||||
RCONPort int
|
||||
Key string
|
||||
Version string
|
||||
ReleaseURL string
|
||||
Checksum string
|
||||
SizeBytes int64
|
||||
TargetKey string
|
||||
ModKey string
|
||||
DLLRef string
|
||||
TargetExecutableChecksum string
|
||||
UE4SSABI string
|
||||
RCONPort int
|
||||
}
|
||||
|
||||
// RuntimeSourceRCONPlan is a frozen, secret-free loopback connection plan for
|
||||
// a ready SCUM UE4SS extension. The generated local config remains Run-owned.
|
||||
// a ready UE4SS extension. The generated local config remains Run-owned.
|
||||
type RuntimeSourceRCONPlan struct {
|
||||
Protocol string
|
||||
ExtensionKey string
|
||||
@@ -543,29 +543,11 @@ type RuntimeServerPrerequisite struct {
|
||||
Kind string
|
||||
}
|
||||
|
||||
// RuntimeServerDeploymentProfile is a legacy game-specific deployment template
|
||||
// declaration kept for backward-compatible manifest decoding.
|
||||
type RuntimeServerDeploymentProfile struct {
|
||||
Key string
|
||||
Version string
|
||||
SupportedTargets []RuntimeTarget
|
||||
SteamAppID string
|
||||
ExecutableKey string
|
||||
InstallRootKey string
|
||||
ConfigKey string
|
||||
ConfigFormat string
|
||||
Prerequisites []RuntimeServerPrerequisite
|
||||
ConfigMappings []RuntimeServerConfigMapping
|
||||
DiscoveryMarkers []RuntimeServerDiscoveryMarker
|
||||
VerificationChecks []RuntimeServerVerificationCheck
|
||||
}
|
||||
|
||||
type GamePluginRuntimeProfiles struct {
|
||||
Discovery []RuntimeDiscoveryProbe
|
||||
LifecycleProfiles []RuntimeLifecycleProfile
|
||||
DependencyProbes []RuntimeDependencyProbe
|
||||
InstallPlans []RuntimeInstallPlan
|
||||
ServerDeployments []RuntimeServerDeploymentProfile
|
||||
LogSources []RuntimeLogSource
|
||||
TransportProfiles []RuntimeTransportProfile
|
||||
DataTargets []RuntimeDataTarget
|
||||
@@ -1826,10 +1808,6 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
||||
profiles.InstallPlans[i].Platforms = CopyStringSlice(profiles.InstallPlans[i].Platforms)
|
||||
profiles.InstallPlans[i].Steps = append([]RuntimeInstallStep(nil), profiles.InstallPlans[i].Steps...)
|
||||
}
|
||||
profiles.ServerDeployments = append([]RuntimeServerDeploymentProfile(nil), profiles.ServerDeployments...)
|
||||
for i := range profiles.ServerDeployments {
|
||||
profiles.ServerDeployments[i] = CopyRuntimeServerDeploymentProfile(profiles.ServerDeployments[i])
|
||||
}
|
||||
profiles.LogSources = append([]RuntimeLogSource(nil), profiles.LogSources...)
|
||||
profiles.TransportProfiles = append([]RuntimeTransportProfile(nil), profiles.TransportProfiles...)
|
||||
for i := range profiles.TransportProfiles {
|
||||
@@ -1895,15 +1873,6 @@ func CopyServerInstance(instance ServerInstance) ServerInstance {
|
||||
return instance
|
||||
}
|
||||
|
||||
func CopyRuntimeServerDeploymentProfile(profile RuntimeServerDeploymentProfile) RuntimeServerDeploymentProfile {
|
||||
profile.Prerequisites = append([]RuntimeServerPrerequisite(nil), profile.Prerequisites...)
|
||||
profile.SupportedTargets = append([]RuntimeTarget(nil), profile.SupportedTargets...)
|
||||
profile.ConfigMappings = append([]RuntimeServerConfigMapping(nil), profile.ConfigMappings...)
|
||||
profile.DiscoveryMarkers = append([]RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...)
|
||||
profile.VerificationChecks = append([]RuntimeServerVerificationCheck(nil), profile.VerificationChecks...)
|
||||
return profile
|
||||
}
|
||||
|
||||
func CopyServerDeploymentProjection(projection ServerDeploymentProjection) ServerDeploymentProjection {
|
||||
projection.DiscoveredFacts = CopyStringMap(projection.DiscoveredFacts)
|
||||
projection.MappingResults = CopyStringMap(projection.MappingResults)
|
||||
|
||||
@@ -82,21 +82,6 @@ type RuntimeServerVerificationCheckBody struct {
|
||||
Required bool `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeServerDeploymentProfileBody struct {
|
||||
Key string `json:"key"`
|
||||
Version string `json:"version"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
SteamAppID string `json:"steamAppId"`
|
||||
ExecutableKey string `json:"executableKey"`
|
||||
InstallRootKey string `json:"installRootKey"`
|
||||
ConfigKey string `json:"configKey"`
|
||||
ConfigFormat string `json:"configFormat"`
|
||||
Prerequisites []RuntimeServerPrerequisiteBody `json:"prerequisites,omitempty"`
|
||||
ConfigMappings []RuntimeServerConfigMappingBody `json:"configMappings"`
|
||||
DiscoveryMarkers []RuntimeServerDiscoveryMarkerBody `json:"discoveryMarkers"`
|
||||
VerificationChecks []RuntimeServerVerificationCheckBody `json:"verificationChecks"`
|
||||
}
|
||||
|
||||
type RuntimeServerPrerequisiteBody struct {
|
||||
Key string `json:"key"`
|
||||
Kind string `json:"kind"`
|
||||
@@ -151,77 +136,77 @@ type RuntimeConfigTemplateBody struct {
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionProfileBody struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Kind string `json:"kind"`
|
||||
Activation string `json:"activation"`
|
||||
Version string `json:"version"`
|
||||
ReleaseState string `json:"releaseState"`
|
||||
ReleaseURL string `json:"releaseUrl,omitempty"`
|
||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
UpdateOnStart bool `json:"updateOnStart"`
|
||||
RCONPort int `json:"rconPort"`
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Kind string `json:"kind"`
|
||||
Activation string `json:"activation"`
|
||||
Version string `json:"version"`
|
||||
ReleaseState string `json:"releaseState"`
|
||||
ReleaseURL string `json:"releaseUrl,omitempty"`
|
||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
UpdateOnStart bool `json:"updateOnStart"`
|
||||
RCONPort int `json:"rconPort"`
|
||||
}
|
||||
|
||||
// RuntimeDLLExtensionProfileResponseBody is the browser-safe projection of a
|
||||
// declared DLL extension. The immutable deployment path, RCON port, and full
|
||||
// release URL remain internal to the manifest/start-job contracts.
|
||||
type RuntimeDLLExtensionProfileResponseBody struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Kind string `json:"kind"`
|
||||
Activation string `json:"activation"`
|
||||
Version string `json:"version"`
|
||||
ReleaseState string `json:"releaseState"`
|
||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
UpdateOnStart bool `json:"updateOnStart"`
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Kind string `json:"kind"`
|
||||
Activation string `json:"activation"`
|
||||
Version string `json:"version"`
|
||||
ReleaseState string `json:"releaseState"`
|
||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
UpdateOnStart bool `json:"updateOnStart"`
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionPlanBody struct {
|
||||
Key string `json:"key"`
|
||||
Version string `json:"version"`
|
||||
ReleaseURL string `json:"releaseUrl"`
|
||||
Checksum string `json:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum"`
|
||||
UE4SSABI string `json:"ue4ssAbi"`
|
||||
RCONPort int `json:"rconPort"`
|
||||
Key string `json:"key"`
|
||||
Version string `json:"version"`
|
||||
ReleaseURL string `json:"releaseUrl"`
|
||||
Checksum string `json:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
TargetExecutableChecksum string `json:"targetExecutableChecksum"`
|
||||
UE4SSABI string `json:"ue4ssAbi"`
|
||||
RCONPort int `json:"rconPort"`
|
||||
}
|
||||
|
||||
type GamePluginRuntimeProfilesBody struct {
|
||||
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
|
||||
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
||||
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
||||
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
||||
ServerDeployments []RuntimeServerDeploymentProfileBody `json:"serverDeployments,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||
ClientManagers *[]json.RawMessage `json:"clientManagers,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
|
||||
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
||||
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
||||
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
||||
ServerDeployments json.RawMessage `json:"serverDeployments,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||
ClientManagers json.RawMessage `json:"clientManagers,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||
}
|
||||
|
||||
func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyClientManagerViolations(prefix string) []string {
|
||||
func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyProfileViolations(prefix string) []string {
|
||||
var violations []string
|
||||
if body.ClientManagers != nil {
|
||||
if len(body.ClientManagers) > 0 {
|
||||
violations = append(violations, prefix+".clientManagers is no longer supported")
|
||||
}
|
||||
for i, profile := range body.LifecycleProfiles {
|
||||
@@ -229,6 +214,9 @@ func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyClientManagerViolatio
|
||||
violations = append(violations, fmt.Sprintf("%s.lifecycleProfiles[%d].clientManagerRef is no longer supported", prefix, i))
|
||||
}
|
||||
}
|
||||
if len(body.ServerDeployments) > 0 {
|
||||
violations = append(violations, prefix+".serverDeployments is no longer supported")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
@@ -240,7 +228,6 @@ type GamePluginRuntimeProfilesResponseBody struct {
|
||||
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
||||
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
||||
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
||||
ServerDeployments []RuntimeServerDeploymentProfileBody `json:"serverDeployments,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||
@@ -265,25 +252,6 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
||||
}
|
||||
profiles.InstallPlans = append(profiles.InstallPlans, plan)
|
||||
}
|
||||
for _, item := range body.ServerDeployments {
|
||||
profile := domain.RuntimeServerDeploymentProfile{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
||||
for _, prerequisite := range item.Prerequisites {
|
||||
profile.Prerequisites = append(profile.Prerequisites, domain.RuntimeServerPrerequisite{Key: prerequisite.Key, Kind: prerequisite.Kind})
|
||||
}
|
||||
for _, target := range item.SupportedTargets {
|
||||
profile.SupportedTargets = append(profile.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
for _, mapping := range item.ConfigMappings {
|
||||
profile.ConfigMappings = append(profile.ConfigMappings, domain.RuntimeServerConfigMapping{FieldKey: mapping.FieldKey, ConfigKey: mapping.ConfigKey, ValueType: mapping.ValueType, Required: mapping.Required})
|
||||
}
|
||||
for _, marker := range item.DiscoveryMarkers {
|
||||
profile.DiscoveryMarkers = append(profile.DiscoveryMarkers, domain.RuntimeServerDiscoveryMarker{Key: marker.Key, Kind: marker.Kind, TargetKey: marker.TargetKey, Expected: marker.Expected, Required: marker.Required})
|
||||
}
|
||||
for _, check := range item.VerificationChecks {
|
||||
profile.VerificationChecks = append(profile.VerificationChecks, domain.RuntimeServerVerificationCheck{Key: check.Key, Kind: check.Kind, TargetKey: check.TargetKey, Required: check.Required})
|
||||
}
|
||||
profiles.ServerDeployments = append(profiles.ServerDeployments, profile)
|
||||
}
|
||||
for _, item := range body.LogSources {
|
||||
profiles.LogSources = append(profiles.LogSources, domain.RuntimeLogSource{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
|
||||
}
|
||||
@@ -294,7 +262,7 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
||||
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
}
|
||||
for _, item := range body.DLLExtensions {
|
||||
extension := domain.RuntimeDLLExtensionProfile{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, SCUMExecutableChecksum: item.SCUMExecutableChecksum, UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart, RCONPort: item.RCONPort}
|
||||
extension := domain.RuntimeDLLExtensionProfile{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, TargetExecutableChecksum: item.TargetExecutableChecksum, UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart, RCONPort: item.RCONPort}
|
||||
for _, target := range item.SupportedTargets {
|
||||
extension.SupportedTargets = append(extension.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
@@ -322,25 +290,6 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
||||
}
|
||||
body.InstallPlans = append(body.InstallPlans, plan)
|
||||
}
|
||||
for _, item := range profiles.ServerDeployments {
|
||||
bodyProfile := RuntimeServerDeploymentProfileBody{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
||||
for _, prerequisite := range item.Prerequisites {
|
||||
bodyProfile.Prerequisites = append(bodyProfile.Prerequisites, RuntimeServerPrerequisiteBody{Key: prerequisite.Key, Kind: prerequisite.Kind})
|
||||
}
|
||||
for _, target := range item.SupportedTargets {
|
||||
bodyProfile.SupportedTargets = append(bodyProfile.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
for _, mapping := range item.ConfigMappings {
|
||||
bodyProfile.ConfigMappings = append(bodyProfile.ConfigMappings, RuntimeServerConfigMappingBody{FieldKey: mapping.FieldKey, ConfigKey: mapping.ConfigKey, ValueType: mapping.ValueType, Required: mapping.Required})
|
||||
}
|
||||
for _, marker := range item.DiscoveryMarkers {
|
||||
bodyProfile.DiscoveryMarkers = append(bodyProfile.DiscoveryMarkers, RuntimeServerDiscoveryMarkerBody{Key: marker.Key, Kind: marker.Kind, TargetKey: marker.TargetKey, Expected: marker.Expected, Required: marker.Required})
|
||||
}
|
||||
for _, check := range item.VerificationChecks {
|
||||
bodyProfile.VerificationChecks = append(bodyProfile.VerificationChecks, RuntimeServerVerificationCheckBody{Key: check.Key, Kind: check.Kind, TargetKey: check.TargetKey, Required: check.Required})
|
||||
}
|
||||
body.ServerDeployments = append(body.ServerDeployments, bodyProfile)
|
||||
}
|
||||
for _, item := range profiles.LogSources {
|
||||
body.LogSources = append(body.LogSources, RuntimeLogSourceBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
|
||||
}
|
||||
@@ -352,7 +301,7 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
||||
}
|
||||
for _, item := range profiles.DLLExtensions {
|
||||
host, filename := safeDLLReleaseLocation(item.ReleaseURL)
|
||||
extension := RuntimeDLLExtensionProfileResponseBody{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseHost: host, ReleaseFilename: filename, Checksum: safeDLLChecksumPrefix(item.Checksum), SizeBytes: item.SizeBytes, SCUMExecutableChecksum: safeDLLChecksumPrefix(item.SCUMExecutableChecksum), UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart}
|
||||
extension := RuntimeDLLExtensionProfileResponseBody{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseHost: host, ReleaseFilename: filename, Checksum: safeDLLChecksumPrefix(item.Checksum), SizeBytes: item.SizeBytes, TargetExecutableChecksum: safeDLLChecksumPrefix(item.TargetExecutableChecksum), UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart}
|
||||
for _, target := range item.SupportedTargets {
|
||||
extension.SupportedTargets = append(extension.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
@@ -364,7 +313,7 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
||||
func dllExtensionPlansFromDomain(plans []domain.RuntimeDLLExtensionPlan) []RuntimeDLLExtensionPlanBody {
|
||||
items := make([]RuntimeDLLExtensionPlanBody, 0, len(plans))
|
||||
for _, item := range plans {
|
||||
items = append(items, RuntimeDLLExtensionPlanBody{Key: item.Key, Version: item.Version, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, SCUMExecutableChecksum: item.SCUMExecutableChecksum, UE4SSABI: item.UE4SSABI, RCONPort: item.RCONPort})
|
||||
items = append(items, RuntimeDLLExtensionPlanBody{Key: item.Key, Version: item.Version, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, TargetExecutableChecksum: item.TargetExecutableChecksum, UE4SSABI: item.UE4SSABI, RCONPort: item.RCONPort})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGamePluginRuntimeProfilesBodyRejectsLegacyServerDeployments(t *testing.T) {
|
||||
body := GamePluginRuntimeProfilesBody{ServerDeployments: json.RawMessage(`[{"key":"legacy"}]`)}
|
||||
|
||||
violations := body.UnsupportedLegacyProfileViolations("runtimeProfiles")
|
||||
if len(violations) != 1 || !strings.Contains(violations[0], "runtimeProfiles.serverDeployments is no longer supported") {
|
||||
t.Fatalf("expected legacy serverDeployments violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGamePluginRuntimeProfilesBodyRejectsLegacyClientManagers(t *testing.T) {
|
||||
body := GamePluginRuntimeProfilesBody{ClientManagers: json.RawMessage(`[{"key":"legacy"}]`), LifecycleProfiles: []RuntimeLifecycleProfileBody{{Key: "local", ClientManagerRef: json.RawMessage(`{"key":"legacy"}`)}}}
|
||||
|
||||
violations := body.UnsupportedLegacyProfileViolations("manifest.runtimeProfiles")
|
||||
if len(violations) != 2 || !strings.Contains(strings.Join(violations, "; "), "clientManagers is no longer supported") || !strings.Contains(strings.Join(violations, "; "), "clientManagerRef is no longer supported") {
|
||||
t.Fatalf("expected legacy client manager violations, got %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGamePluginRuntimeProfilesBodyRejectsLegacyClientManagersWhenNull(t *testing.T) {
|
||||
var body GamePluginRuntimeProfilesBody
|
||||
if err := json.Unmarshal([]byte(`{"clientManagers":null}`), &body); err != nil {
|
||||
t.Fatalf("decode runtime profile body: %v", err)
|
||||
}
|
||||
|
||||
violations := body.UnsupportedLegacyProfileViolations("runtimeProfiles")
|
||||
if len(violations) != 1 || !strings.Contains(violations[0], "runtimeProfiles.clientManagers is no longer supported") {
|
||||
t.Fatalf("expected legacy clientManagers null violation, got %v", violations)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ Platform owns the reviewable dependency catalog, immutable plan digest, selected
|
||||
|
||||
1. `GET /api/v1/server-instances/{id}/dependencies` resolves the installed plugin version, complete runtime binding, online Run endpoint OS/architecture, target-matched probes/plans, and canonical SHA-256 digest.
|
||||
2. An install request must submit that exact digest. Platform re-resolves the declaration before creating `dependencies.install`; missing or stale plan evidence is denied and reported.
|
||||
3. Run retrieves private input through signed `POST /api/v1/run/jobs/dependency-input` only for the active endpoint/session/attempt/lease and non-cancelled job. It executes closed command-version, Java, Docker, package, service, Steam, file, package-manager, verified HTTPS download, and SteamCMD adapters with bounded output/timeouts and a durable step journal.
|
||||
4. Terminal evidence is typed and redacted. Platform verifies probe key, plan digest, result checksum, and job attempt before updating `DependencyStatus`.
|
||||
3. Run retrieves private input through signed `POST /api/v1/run/jobs/dependency-input` only for the active endpoint/session/attempt/lease and non-cancelled job. It executes closed command-version, Java, Docker, package, service, file, package-manager, and verified HTTPS download adapters with bounded output/timeouts and a durable step journal.
|
||||
4. Terminal evidence is typed dependency status, not log parsing. Platform verifies probe key, plan digest, result checksum, and job attempt before updating `DependencyStatus`.
|
||||
|
||||
## Self-update flow
|
||||
|
||||
|
||||
@@ -26,11 +26,9 @@ from command text. Empty `shell` means argv-oriented execution.
|
||||
|
||||
Game plugins own concrete game policy: install/update commands, app ids,
|
||||
executable refs, default launch flags, stop-before-update behavior, and startup
|
||||
argument construction. For SCUM, the plugin action assets own the SteamCMD flow:
|
||||
stop `SCUMServer.exe` when updating, keep SteamCMD outside the server install
|
||||
root, run
|
||||
`steamcmd.exe +force_install_dir <serverRoot> +login anonymous +app_update 3792580 +quit`,
|
||||
and start `<serverRoot>\\SCUM\\Binaries\\Win64\\SCUMServer.exe -port=<gamePort> -MaxPlayers=<maxPlayers> -log`.
|
||||
argument construction. For SCUM, those values live in the SCUM plugin action
|
||||
assets and scripts; Platform and Run only pass the bounded deployment context to
|
||||
the declared lifecycle action and execute it through generic action handling.
|
||||
|
||||
Generated Run distributions carry validated plugin lifecycle assets into the
|
||||
server-scoped workspace. Run materializes those assets at startup and executes
|
||||
|
||||
@@ -208,13 +208,6 @@ func (svc *CoreService) ReadRunUpdateChunk(request domain.RunUpdateChunkRequest)
|
||||
if err != nil {
|
||||
return domain.RunUpdateChunk{}, err
|
||||
}
|
||||
payload, err := svc.artifactPayload(artifact.ID)
|
||||
if err != nil {
|
||||
return domain.RunUpdateChunk{}, err
|
||||
}
|
||||
if int64(len(payload)) != artifact.SizeBytes || validator.BytesChecksum(payload) != artifact.Checksum {
|
||||
return domain.RunUpdateChunk{}, validationError("update artifact content does not match metadata")
|
||||
}
|
||||
if request.Offset >= artifact.SizeBytes {
|
||||
return domain.RunUpdateChunk{}, validationError("offset must be inside update artifact")
|
||||
}
|
||||
@@ -224,7 +217,11 @@ func (svc *CoreService) ReadRunUpdateChunk(request domain.RunUpdateChunkRequest)
|
||||
length = int(remaining)
|
||||
}
|
||||
end := request.Offset + int64(length)
|
||||
return domain.CopyRunUpdateChunk(domain.RunUpdateChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload[int(request.Offset):int(end)], Complete: end == artifact.SizeBytes}), nil
|
||||
payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, request.Offset, length)
|
||||
if err != nil {
|
||||
return domain.RunUpdateChunk{}, err
|
||||
}
|
||||
return domain.CopyRunUpdateChunk(domain.RunUpdateChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload, Complete: end == artifact.SizeBytes}), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) activeFencedInputJob(endpointID, sessionToken, jobID, leaseToken string, attempt int) (domain.Job, error) {
|
||||
|
||||
@@ -231,6 +231,10 @@ func TestRunUpdateTargetFencingChunksHealthAndRollbackProjection(t *testing.T) {
|
||||
if err != nil || string(chunk.Payload) != string(payload[:8]) || chunk.Offset != 0 || chunk.TotalBytes != int64(len(payload)) {
|
||||
t.Fatalf("read bounded update chunk: chunk=%+v err=%v", chunk, err)
|
||||
}
|
||||
secondChunk, err := svc.ReadRunUpdateChunk(domain.RunUpdateChunkRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 8, Length: 5})
|
||||
if err != nil || string(secondChunk.Payload) != string(payload[8:13]) || secondChunk.Offset != 8 || secondChunk.Complete {
|
||||
t.Fatalf("read offset update chunk: chunk=%+v err=%v", secondChunk, err)
|
||||
}
|
||||
if _, err := svc.ReadRunUpdateChunk(domain.RunUpdateChunkRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt + 1, Offset: 0, Length: 8}); err == nil {
|
||||
t.Fatal("expected stale update chunk attempt rejection")
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ func autonomousRunLogSourceKind(kind string) bool {
|
||||
}
|
||||
|
||||
func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.RunAutonomousDLLExtension {
|
||||
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, TargetExecutableChecksum: extension.TargetExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||
}
|
||||
|
||||
func autonomousDataTarget(target domain.RuntimeDataTarget) domain.RunAutonomousDataTarget {
|
||||
|
||||
@@ -782,7 +782,7 @@ func runReleasePlatformURL() string {
|
||||
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
|
||||
return value
|
||||
}
|
||||
return "https://scum.npc0.com/"
|
||||
return "http://127.0.0.1:8080/"
|
||||
}
|
||||
|
||||
func distributionID(prefix string, parts ...interface{}) string {
|
||||
|
||||
@@ -973,6 +973,45 @@ func TestPluginFileWorkspaceDoesNotConstrainServerFileDispatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRunFileInputChunkReadsRequestedRange(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
plugin.FileWorkspace = scumTestFileWorkspace()
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin workspace: %v", err)
|
||||
}
|
||||
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-chunk", DisplayName: "File Chunk Owner", Email: "file-chunk@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-chunk", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Chunk Server", State: domain.ServerInstanceStateRunning})
|
||||
if err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||
payload := []byte("0123456789abcdef")
|
||||
upload, err := svc.UploadServerFileForSession(ownerSession, domain.ServerFileUploadRequest{ServerInstanceID: instance.ID, DirectoryKey: "scum-logs", Filename: "chunked.log", Payload: payload, Checksum: validator.BytesChecksum(payload), IdempotencyKey: "file-chunk-upload"})
|
||||
if err != nil {
|
||||
t.Fatalf("upload server file: %v", err)
|
||||
}
|
||||
hello := validRunControlHello()
|
||||
hello.RunEndpointID = endpoint.ID
|
||||
hello.CapabilityReport.Capabilities = []string{domain.JobCapabilityFilesWrite}
|
||||
registered, err := svc.RegisterRunHello(hello)
|
||||
if err != nil {
|
||||
t.Fatalf("register run: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, Capabilities: []string{domain.JobCapabilityFilesWrite}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != upload.Job.ID {
|
||||
t.Fatalf("claim file write: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
chunk, err := svc.ReadRunFileInputChunk(domain.RunFileInputChunkRequest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 4, Length: 6})
|
||||
if err != nil || string(chunk.Payload) != string(payload[4:10]) || chunk.Offset != 4 || chunk.Complete {
|
||||
t.Fatalf("read offset file input chunk: chunk=%+v err=%v", chunk, err)
|
||||
}
|
||||
finalChunk, err := svc.ReadRunFileInputChunk(domain.RunFileInputChunkRequest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 14, Length: 8})
|
||||
if err != nil || string(finalChunk.Payload) != string(payload[14:]) || !finalChunk.Complete {
|
||||
t.Fatalf("read final file input chunk: chunk=%+v err=%v", finalChunk, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerFileListReportsFailedRuntimeRefresh(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
|
||||
@@ -392,13 +392,6 @@ func (svc *CoreService) ReadRunFileInputChunk(request domain.RunFileInputChunkRe
|
||||
if artifact.OwnerKind != domain.ArtifactOwnerKindServerInstance || artifact.OwnerID != serverInstanceID || artifact.State != domain.ArtifactStateAvailable {
|
||||
return domain.RunFileInputChunk{}, ErrForbidden
|
||||
}
|
||||
payload, err := svc.artifactPayload(artifact.ID)
|
||||
if err != nil {
|
||||
return domain.RunFileInputChunk{}, err
|
||||
}
|
||||
if int64(len(payload)) != artifact.SizeBytes || validator.BytesChecksum(payload) != artifact.Checksum {
|
||||
return domain.RunFileInputChunk{}, validationError("file input artifact checksum mismatch")
|
||||
}
|
||||
if request.Offset >= artifact.SizeBytes {
|
||||
return domain.RunFileInputChunk{}, validationError("offset must be inside artifact content")
|
||||
}
|
||||
@@ -407,8 +400,12 @@ func (svc *CoreService) ReadRunFileInputChunk(request domain.RunFileInputChunkRe
|
||||
if int64(length) > remaining {
|
||||
length = int(remaining)
|
||||
}
|
||||
end := int(request.Offset) + length
|
||||
chunk := domain.RunFileInputChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload[int(request.Offset):end], Complete: int64(end) == artifact.SizeBytes}
|
||||
payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, request.Offset, length)
|
||||
if err != nil {
|
||||
return domain.RunFileInputChunk{}, err
|
||||
}
|
||||
end := request.Offset + int64(length)
|
||||
chunk := domain.RunFileInputChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload, Complete: end == artifact.SizeBytes}
|
||||
return domain.CopyRunFileInputChunk(chunk), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -461,7 +461,7 @@ func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profi
|
||||
if !runtimeDLLExtensionSupportsTarget(extension, endpoint.Platform, endpoint.Architecture) {
|
||||
return nil, validationError("unsupported_extension_platform: UE4SS DLL requires windows/amd64")
|
||||
}
|
||||
plans = append(plans, domain.RuntimeDLLExtensionPlan{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort})
|
||||
plans = append(plans, domain.RuntimeDLLExtensionPlan{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, TargetExecutableChecksum: extension.TargetExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort})
|
||||
}
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
@@ -466,7 +466,7 @@ func attachReadyLifecycleDLLExtension(t *testing.T, svc *CoreService, plugin *do
|
||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
}}
|
||||
if err := svc.store.GamePlugins().Update(*plugin); err != nil {
|
||||
t.Fatalf("attach ready DLL extension: %v", err)
|
||||
|
||||
@@ -280,7 +280,7 @@ func newSourceRCONFixtureWithRuntimeBinding(t *testing.T, createBinding bool) (*
|
||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
}},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestValidateGamePluginRuntimeProfilesRejectsUnsafeOrUnpublishedDLLExtension
|
||||
func TestValidateJobRejectsDLLPlanOutsideProcessStart(t *testing.T) {
|
||||
profiles := validRuntimeDLLExtensionProfiles()
|
||||
extension := profiles.DLLExtensions[0]
|
||||
job := domain.Job{ID: "dll-job", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: domain.LifecycleCapabilityStop, IdempotencyKey: "dll-stop", State: domain.JobStateQueued, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{LifecycleOperation: "stop", DLLExtensions: []domain.RuntimeDLLExtensionPlan{{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}}}}
|
||||
job := domain.Job{ID: "dll-job", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: domain.LifecycleCapabilityStop, IdempotencyKey: "dll-stop", State: domain.JobStateQueued, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{LifecycleOperation: "stop", DLLExtensions: []domain.RuntimeDLLExtensionPlan{{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, TargetExecutableChecksum: extension.TargetExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}}}}
|
||||
if err := ValidateJob(job); err == nil || !strings.Contains(err.Error(), "process.start") {
|
||||
t.Fatalf("expected process.start plan restriction, got %v", err)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func validRuntimeDLLExtensionProfiles() domain.GamePluginRuntimeProfiles {
|
||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
discoveryKeys := map[string]struct{}{}
|
||||
dependencyKeys := map[string]struct{}{}
|
||||
installPlanKeys := map[string]struct{}{}
|
||||
serverDeploymentKeys := map[string]struct{}{}
|
||||
logSourceKeys := map[string]struct{}{}
|
||||
|
||||
for i, probe := range profiles.Discovery {
|
||||
@@ -95,7 +94,7 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
}
|
||||
for j, step := range plan.Steps {
|
||||
stepPrefix := fmt.Sprintf("%s.steps[%d]", prefix, j)
|
||||
if !oneOf(step.Type, "package", "verified-download", "steamcmd-app", "manual") {
|
||||
if !oneOf(step.Type, "package", "verified-download", "manual") {
|
||||
violations = append(violations, stepPrefix+".type is invalid")
|
||||
}
|
||||
violations = append(violations, validateProfileKey(stepPrefix+".targetKey", step.TargetKey)...)
|
||||
@@ -133,100 +132,14 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
if step.DownloadRef == "" || step.Checksum == "" {
|
||||
violations = append(violations, stepPrefix+" requires downloadRef and checksum")
|
||||
}
|
||||
case "steamcmd-app":
|
||||
if step.PackageManager != "" && step.PackageManager != "steamcmd" || !regexp.MustCompile(`^[0-9]{1,12}$`).MatchString(step.PackageName) {
|
||||
violations = append(violations, stepPrefix+" requires a numeric Steam app and steamcmd adapter")
|
||||
}
|
||||
case "manual":
|
||||
if step.DownloadRef != "" || step.Checksum != "" || step.PackageName != "" {
|
||||
if step.DownloadRef != "" || step.Checksum != "" || step.PackageManager != "" || step.PackageName != "" || step.Version != "" {
|
||||
violations = append(violations, stepPrefix+" manual step cannot contain machine execution fields")
|
||||
}
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", plan.Platforms)...)
|
||||
}
|
||||
for i, profile := range profiles.ServerDeployments {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.serverDeployments[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", profile.Key)...)
|
||||
violations = append(violations, recordRuntimeProfileKey(serverDeploymentKeys, prefix+".key", profile.Key)...)
|
||||
if !validSemanticVersion(profile.Version) {
|
||||
violations = append(violations, prefix+".version must be semantic")
|
||||
}
|
||||
if !regexp.MustCompile(`^[0-9]{1,12}$`).MatchString(profile.SteamAppID) {
|
||||
violations = append(violations, prefix+".steamAppId must be numeric")
|
||||
}
|
||||
for field, value := range map[string]string{"executableKey": profile.ExecutableKey, "installRootKey": profile.InstallRootKey, "configKey": profile.ConfigKey} {
|
||||
violations = append(violations, validateProfileKey(prefix+"."+field, value)...)
|
||||
}
|
||||
if profile.ConfigFormat != "ini" && profile.ConfigFormat != "json" && profile.ConfigFormat != "yaml" && profile.ConfigFormat != "properties" {
|
||||
violations = append(violations, prefix+".configFormat is invalid")
|
||||
}
|
||||
if len(profile.SupportedTargets) == 0 {
|
||||
violations = append(violations, prefix+".supportedTargets must not be empty")
|
||||
}
|
||||
prerequisiteKeys := map[string]struct{}{}
|
||||
for j, prerequisite := range profile.Prerequisites {
|
||||
prerequisitePrefix := fmt.Sprintf("%s.prerequisites[%d]", prefix, j)
|
||||
violations = append(violations, validateProfileKey(prerequisitePrefix+".key", prerequisite.Key)...)
|
||||
if _, exists := prerequisiteKeys[prerequisite.Key]; exists {
|
||||
violations = append(violations, prerequisitePrefix+".key duplicates another prerequisite")
|
||||
}
|
||||
prerequisiteKeys[prerequisite.Key] = struct{}{}
|
||||
if !oneOf(prerequisite.Kind, "steamcmd", "windows-vcredist", "windows-directx") {
|
||||
violations = append(violations, prerequisitePrefix+".kind is invalid")
|
||||
}
|
||||
}
|
||||
for j, target := range profile.SupportedTargets {
|
||||
if !validPluginSupportedOS(target.OS) || !oneOf(target.Arch, "amd64", "arm64") {
|
||||
violations = append(violations, fmt.Sprintf("%s.supportedTargets[%d] is invalid", prefix, j))
|
||||
}
|
||||
}
|
||||
mappingKeys := map[string]struct{}{}
|
||||
for j, mapping := range profile.ConfigMappings {
|
||||
mappingPrefix := fmt.Sprintf("%s.configMappings[%d]", prefix, j)
|
||||
if !regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._/-]{0,79}$`).MatchString(mapping.FieldKey) {
|
||||
violations = append(violations, mappingPrefix+".fieldKey is invalid")
|
||||
}
|
||||
violations = append(violations, validateProfileKey(mappingPrefix+".configKey", mapping.ConfigKey)...)
|
||||
if _, exists := mappingKeys[mapping.FieldKey]; exists {
|
||||
violations = append(violations, mappingPrefix+".fieldKey duplicates another mapping")
|
||||
}
|
||||
mappingKeys[mapping.FieldKey] = struct{}{}
|
||||
if !oneOf(mapping.ValueType, "text", "integer", "number", "boolean", "port") {
|
||||
violations = append(violations, mappingPrefix+".valueType is invalid")
|
||||
}
|
||||
}
|
||||
markerKeys := map[string]struct{}{}
|
||||
for j, marker := range profile.DiscoveryMarkers {
|
||||
markerPrefix := fmt.Sprintf("%s.discoveryMarkers[%d]", prefix, j)
|
||||
violations = append(violations, validateProfileKey(markerPrefix+".key", marker.Key)...)
|
||||
violations = append(violations, validateProfileKey(markerPrefix+".targetKey", marker.TargetKey)...)
|
||||
if _, exists := markerKeys[marker.Key]; exists {
|
||||
violations = append(violations, markerPrefix+".key duplicates another marker")
|
||||
}
|
||||
markerKeys[marker.Key] = struct{}{}
|
||||
if !oneOf(marker.Kind, "file.exists", "command.version", "port.open", "steam.app") {
|
||||
violations = append(violations, markerPrefix+".kind is invalid")
|
||||
}
|
||||
violations = append(violations, validateSafeRuntimeValue(markerPrefix+".expected", marker.Expected)...)
|
||||
}
|
||||
checkKeys := map[string]struct{}{}
|
||||
for j, check := range profile.VerificationChecks {
|
||||
checkPrefix := fmt.Sprintf("%s.verificationChecks[%d]", prefix, j)
|
||||
violations = append(violations, validateProfileKey(checkPrefix+".key", check.Key)...)
|
||||
violations = append(violations, validateProfileKey(checkPrefix+".targetKey", check.TargetKey)...)
|
||||
if _, exists := checkKeys[check.Key]; exists {
|
||||
violations = append(violations, checkPrefix+".key duplicates another check")
|
||||
}
|
||||
checkKeys[check.Key] = struct{}{}
|
||||
if !oneOf(check.Kind, "executable.present", "version.matches", "port.bound", "config.readable", "process.healthy") {
|
||||
violations = append(violations, checkPrefix+".kind is invalid")
|
||||
}
|
||||
}
|
||||
if len(profile.VerificationChecks) == 0 || !containsRequiredVerification(profile.VerificationChecks) {
|
||||
violations = append(violations, prefix+".verificationChecks must include executable, config, port, and process checks")
|
||||
}
|
||||
}
|
||||
for i, source := range profiles.LogSources {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.logSources[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", source.Key)...)
|
||||
@@ -329,23 +242,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func containsRequiredVerification(checks []domain.RuntimeServerVerificationCheck) bool {
|
||||
required := map[string]bool{"executable.present": false, "port.bound": false, "config.readable": false, "process.healthy": false}
|
||||
for _, check := range checks {
|
||||
if check.Required {
|
||||
if _, ok := required[check.Kind]; ok {
|
||||
required[check.Kind] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, present := range required {
|
||||
if !present {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateRuntimeDLLExtensionProfile(prefix string, extension domain.RuntimeDLLExtensionProfile) []string {
|
||||
var violations []string
|
||||
if extension.Kind != "ue4ss-dll" || extension.Activation != "server-start" {
|
||||
@@ -381,8 +277,8 @@ func validateRuntimeDLLExtensionProfile(prefix string, extension domain.RuntimeD
|
||||
if extension.ReleaseURL == "" {
|
||||
violations = append(violations, prefix+".releaseUrl is required for a ready release")
|
||||
}
|
||||
if !validSHA256Checksum(extension.Checksum) || !validSHA256Checksum(extension.SCUMExecutableChecksum) {
|
||||
violations = append(violations, prefix+".checksum and scumExecutableChecksum must be SHA-256")
|
||||
if !validSHA256Checksum(extension.Checksum) || !validSHA256Checksum(extension.TargetExecutableChecksum) {
|
||||
violations = append(violations, prefix+".checksum and targetExecutableChecksum must be SHA-256")
|
||||
}
|
||||
if extension.SizeBytes < 1 || extension.SizeBytes > 128*1024*1024 {
|
||||
violations = append(violations, prefix+".sizeBytes is out of bounds")
|
||||
@@ -409,22 +305,22 @@ func validateRuntimeDLLReleaseURL(field string, value string) []string {
|
||||
|
||||
func validateRuntimeDLLExtensionPlan(prefix string, plan domain.RuntimeDLLExtensionPlan) []string {
|
||||
return validateRuntimeDLLExtensionProfile(prefix, domain.RuntimeDLLExtensionProfile{
|
||||
Key: plan.Key,
|
||||
Kind: "ue4ss-dll",
|
||||
Activation: "server-start",
|
||||
Version: plan.Version,
|
||||
ReleaseState: "ready",
|
||||
ReleaseURL: plan.ReleaseURL,
|
||||
Checksum: plan.Checksum,
|
||||
SizeBytes: plan.SizeBytes,
|
||||
TargetKey: plan.TargetKey,
|
||||
ModKey: plan.ModKey,
|
||||
DLLRef: plan.DLLRef,
|
||||
SCUMExecutableChecksum: plan.SCUMExecutableChecksum,
|
||||
UE4SSABI: plan.UE4SSABI,
|
||||
SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}},
|
||||
UpdateOnStart: true,
|
||||
RCONPort: plan.RCONPort,
|
||||
Key: plan.Key,
|
||||
Kind: "ue4ss-dll",
|
||||
Activation: "server-start",
|
||||
Version: plan.Version,
|
||||
ReleaseState: "ready",
|
||||
ReleaseURL: plan.ReleaseURL,
|
||||
Checksum: plan.Checksum,
|
||||
SizeBytes: plan.SizeBytes,
|
||||
TargetKey: plan.TargetKey,
|
||||
ModKey: plan.ModKey,
|
||||
DLLRef: plan.DLLRef,
|
||||
TargetExecutableChecksum: plan.TargetExecutableChecksum,
|
||||
UE4SSABI: plan.UE4SSABI,
|
||||
SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}},
|
||||
UpdateOnStart: true,
|
||||
RCONPort: plan.RCONPort,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestValidateGamePluginRuntimeProfilesRejectsLegacySteamCMDAppStep(t *testing.T) {
|
||||
profiles := domain.GamePluginRuntimeProfiles{InstallPlans: []domain.RuntimeInstallPlan{{Key: "install-game", Title: "Install game", Steps: []domain.RuntimeInstallStep{{Type: "steamcmd-app", TargetKey: "server/install-root", PackageManager: "steamcmd", PackageName: "123456"}}}}}
|
||||
|
||||
err := ValidateGamePluginRuntimeProfiles(profiles)
|
||||
if err == nil || !strings.Contains(err.Error(), "type is invalid") {
|
||||
t.Fatalf("expected legacy steamcmd-app step rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginRuntimeProfilesRejectsManualStepExecutionFields(t *testing.T) {
|
||||
profiles := domain.GamePluginRuntimeProfiles{InstallPlans: []domain.RuntimeInstallPlan{{Key: "manual-plan", Title: "Manual plan", Steps: []domain.RuntimeInstallStep{{Type: "manual", TargetKey: "operator/manual", PackageManager: "steamcmd"}}}}}
|
||||
|
||||
err := ValidateGamePluginRuntimeProfiles(profiles)
|
||||
if err == nil || !strings.Contains(err.Error(), "manual step cannot contain machine execution fields") {
|
||||
t.Fatalf("expected manual execution field rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user