Fix local run freshness and plugin asset handling
This commit is contained in:
@@ -166,7 +166,7 @@ func TestMetricsAndConfigReadAPIAreSafeAndRoleScoped(t *testing.T) {
|
|||||||
assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/metrics/platform", "", ownerSession), http.StatusForbidden, errorCodeForbidden)
|
assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/metrics/platform", "", ownerSession), http.StatusForbidden, errorCodeForbidden)
|
||||||
|
|
||||||
ownerMetrics := getJSONWithAuth[dto.ServerMetricsListResponse](t, router, "/api/v1/metrics/server-instances", ownerSession)
|
ownerMetrics := getJSONWithAuth[dto.ServerMetricsListResponse](t, router, "/api/v1/metrics/server-instances", ownerSession)
|
||||||
if ownerMetrics.Count != 1 || ownerMetrics.Items[0].ServerInstanceID != instance.ID || !ownerMetrics.Items[0].Online || ownerMetrics.Items[0].CPUPercent != nil || ownerMetrics.Items[0].Source != "run-metrics-pending" {
|
if ownerMetrics.Count != 1 || ownerMetrics.Items[0].ServerInstanceID != instance.ID || ownerMetrics.Items[0].Online || ownerMetrics.Items[0].CPUPercent != nil || ownerMetrics.Items[0].Source != "run-metrics-pending" {
|
||||||
t.Fatalf("unexpected owner metrics: %+v", ownerMetrics)
|
t.Fatalf("unexpected owner metrics: %+v", ownerMetrics)
|
||||||
}
|
}
|
||||||
otherMetrics := getJSONWithAuth[dto.ServerMetricsListResponse](t, router, "/api/v1/metrics/server-instances", otherSession)
|
otherMetrics := getJSONWithAuth[dto.ServerMetricsListResponse](t, router, "/api/v1/metrics/server-instances", otherSession)
|
||||||
|
|||||||
@@ -634,6 +634,7 @@ type GamePluginManifestRegistration struct {
|
|||||||
type PluginAssetFile struct {
|
type PluginAssetFile struct {
|
||||||
Path string
|
Path string
|
||||||
Content string
|
Content string
|
||||||
|
Encoding string
|
||||||
Mode int
|
Mode int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ type GamePluginManifestRegistrationRequest struct {
|
|||||||
type PluginAssetFileBody struct {
|
type PluginAssetFileBody struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
|
Encoding string `json:"encoding,omitempty"`
|
||||||
Mode int `json:"mode,omitempty"`
|
Mode int `json:"mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,7 +1040,7 @@ func pluginAssetFilesToDomain(files []PluginAssetFileBody) []domain.PluginAssetF
|
|||||||
}
|
}
|
||||||
out := make([]domain.PluginAssetFile, len(files))
|
out := make([]domain.PluginAssetFile, len(files))
|
||||||
for i, file := range files {
|
for i, file := range files {
|
||||||
out[i] = domain.PluginAssetFile{Path: file.Path, Content: file.Content, Mode: file.Mode}
|
out[i] = domain.PluginAssetFile{Path: file.Path, Content: file.Content, Encoding: file.Encoding, Mode: file.Mode}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func TestGamePluginManifestRegistrationToDomainCopiesSlices(t *testing.T) {
|
|||||||
AI: GamePluginManifestAIBody{Purposes: []string{"logs.diagnose"}},
|
AI: GamePluginManifestAIBody{Purposes: []string{"logs.diagnose"}},
|
||||||
},
|
},
|
||||||
AssetFiles: []PluginAssetFileBody{
|
AssetFiles: []PluginAssetFileBody{
|
||||||
{Path: "actions/install.json", Content: "{}", Mode: 0o600},
|
{Path: "actions/install.json", Content: "{}", Encoding: "base64", Mode: 0o600},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ func TestGamePluginManifestRegistrationToDomainCopiesSlices(t *testing.T) {
|
|||||||
domainRegistration.Manifest.Pages[0].Permissions[0] = "ai.invoke"
|
domainRegistration.Manifest.Pages[0].Permissions[0] = "ai.invoke"
|
||||||
domainRegistration.Manifest.AI.Purposes[0] = "config.suggest"
|
domainRegistration.Manifest.AI.Purposes[0] = "config.suggest"
|
||||||
|
|
||||||
if request.Manifest.Tags[0] != "example" || request.Manifest.Server.SupportedOS[0] != "linux" || request.Manifest.AssetFiles[0].Path != "actions/install.json" || request.AssetFiles[0].Content != "{}" || request.Manifest.Pages[0].Permissions[0] != "server.logs.read" || request.Manifest.AI.Purposes[0] != "logs.diagnose" {
|
if request.Manifest.Tags[0] != "example" || request.Manifest.Server.SupportedOS[0] != "linux" || request.Manifest.AssetFiles[0].Path != "actions/install.json" || request.AssetFiles[0].Content != "{}" || request.AssetFiles[0].Encoding != "base64" || request.Manifest.Pages[0].Permissions[0] != "server.logs.read" || request.Manifest.AI.Purposes[0] != "logs.diagnose" {
|
||||||
t.Fatalf("expected manifest request slices to be copied, got %+v", request)
|
t.Fatalf("expected manifest request slices to be copied, got %+v", request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ func (svc *CoreService) authorizeClientManagerLifecycle(sessionID, serverInstanc
|
|||||||
if err != nil || profile.Deployment.Mode != "run-supervised" || !containsString(profile.Deployment.RequiredRunCapabilities, capability) {
|
if err != nil || profile.Deployment.Mode != "run-supervised" || !containsString(profile.Deployment.RequiredRunCapabilities, capability) {
|
||||||
return domain.User{}, domain.ServerInstance{}, domain.GamePlugin{}, domain.RuntimeClientManagerProfile{}, domain.RunEndpoint{}, ErrForbidden
|
return domain.User{}, domain.ServerInstance{}, domain.GamePlugin{}, domain.RuntimeClientManagerProfile{}, domain.RunEndpoint{}, ErrForbidden
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.User{}, domain.ServerInstance{}, domain.GamePlugin{}, domain.RuntimeClientManagerProfile{}, domain.RunEndpoint{}, err
|
return domain.User{}, domain.ServerInstance{}, domain.GamePlugin{}, domain.RuntimeClientManagerProfile{}, domain.RunEndpoint{}, err
|
||||||
}
|
}
|
||||||
@@ -1027,11 +1027,11 @@ func (svc *CoreService) clientManagerLifecycleView(installation domain.ClientMan
|
|||||||
}
|
}
|
||||||
profile, _ := findRuntimeClientManagerProfile(plugin, installation.ProfileKey)
|
profile, _ := findRuntimeClientManagerProfile(plugin, installation.ProfileKey)
|
||||||
endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||||
view.Actions = clientManagerLifecycleActions(installation, view.Distribution, profile, endpoint, endpointErr)
|
view.Actions = svc.clientManagerLifecycleActions(installation, view.Distribution, profile, endpoint, endpointErr)
|
||||||
return domain.CopyClientManagerLifecycleView(view), nil
|
return domain.CopyClientManagerLifecycleView(view), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientManagerLifecycleActions(installation domain.ClientManagerInstallation, distribution domain.ClientManagerDistribution, profile domain.RuntimeClientManagerProfile, endpoint domain.RunEndpoint, endpointErr error) []domain.ClientManagerLifecycleActionAvailability {
|
func (svc *CoreService) clientManagerLifecycleActions(installation domain.ClientManagerInstallation, distribution domain.ClientManagerDistribution, profile domain.RuntimeClientManagerProfile, endpoint domain.RunEndpoint, endpointErr error) []domain.ClientManagerLifecycleActionAvailability {
|
||||||
operations := []domain.ClientManagerLifecycleOperation{domain.ClientManagerOperationDeploy, domain.ClientManagerOperationStart, domain.ClientManagerOperationStop, domain.ClientManagerOperationRestart, domain.ClientManagerOperationStatus, domain.ClientManagerOperationUpdate, domain.ClientManagerOperationRollback, domain.ClientManagerOperationUninstall}
|
operations := []domain.ClientManagerLifecycleOperation{domain.ClientManagerOperationDeploy, domain.ClientManagerOperationStart, domain.ClientManagerOperationStop, domain.ClientManagerOperationRestart, domain.ClientManagerOperationStatus, domain.ClientManagerOperationUpdate, domain.ClientManagerOperationRollback, domain.ClientManagerOperationUninstall}
|
||||||
items := make([]domain.ClientManagerLifecycleActionAvailability, 0, len(operations))
|
items := make([]domain.ClientManagerLifecycleActionAvailability, 0, len(operations))
|
||||||
for _, operation := range operations {
|
for _, operation := range operations {
|
||||||
@@ -1046,7 +1046,7 @@ func clientManagerLifecycleActions(installation domain.ClientManagerInstallation
|
|||||||
case domain.ClientManagerOperationUninstall:
|
case domain.ClientManagerOperationUninstall:
|
||||||
capability = domain.JobCapabilityClientManagerUninstall
|
capability = domain.JobCapabilityClientManagerUninstall
|
||||||
}
|
}
|
||||||
available := endpointErr == nil && endpoint.Status == domain.RunEndpointStatusOnline && containsString(endpoint.Capabilities, capability) && containsString(profile.Deployment.RequiredRunCapabilities, capability)
|
available := endpointErr == nil && endpoint.Status == domain.RunEndpointStatusOnline && svc.runEndpointHeartbeatCurrent(endpoint) && containsString(endpoint.Capabilities, capability) && containsString(profile.Deployment.RequiredRunCapabilities, capability)
|
||||||
reason := ""
|
reason := ""
|
||||||
if !available {
|
if !available {
|
||||||
reason = "assigned Run endpoint is offline or lacks the declared capability"
|
reason = "assigned Run endpoint is offline or lacks the declared capability"
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ func (svc *CoreService) resolveDependencyContext(serverInstanceID string) (depen
|
|||||||
if binding.Status != domain.RuntimeBindingStatusComplete || binding.PluginVersion != plugin.Version {
|
if binding.Status != domain.RuntimeBindingStatusComplete || binding.PluginVersion != plugin.Version {
|
||||||
return dependencyResolution{}, validationError("runtime binding is incomplete or stale")
|
return dependencyResolution{}, validationError("runtime binding is incomplete or stale")
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dependencyResolution{}, err
|
return dependencyResolution{}, err
|
||||||
}
|
}
|
||||||
@@ -410,7 +410,7 @@ func (svc *CoreService) resolveRunUpdate(job domain.Job) (domain.RunUpdateJob, d
|
|||||||
if distribution.ID == "" || distribution.RunEndpointID != job.RunEndpointID || distribution.TargetOS != update.TargetOS || distribution.TargetArch != update.TargetArch || distribution.Checksum != update.Checksum {
|
if distribution.ID == "" || distribution.RunEndpointID != job.RunEndpointID || distribution.TargetOS != update.TargetOS || distribution.TargetArch != update.TargetArch || distribution.Checksum != update.Checksum {
|
||||||
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, validationError("Run distribution no longer matches update")
|
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, validationError("Run distribution no longer matches update")
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(job.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(job.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, err
|
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, err
|
||||||
}
|
}
|
||||||
@@ -551,7 +551,7 @@ func (svc *CoreService) ReportRunUpdateHealth(report domain.RunUpdateHealthRepor
|
|||||||
if update.ID == "" || job.ExecutionResult.Checksum != update.Checksum {
|
if update.ID == "" || job.ExecutionResult.Checksum != update.Checksum {
|
||||||
return domain.RunUpdateHealthResult{}, validationError("Run update health report does not match staged update")
|
return domain.RunUpdateHealthResult{}, validationError("Run update health report does not match staged update")
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(report.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(report.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.RunUpdateHealthResult{}, err
|
return domain.RunUpdateHealthResult{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,11 +378,12 @@ func encodePluginWorkspaceSeed(files []domain.PluginAssetFile) (string, error) {
|
|||||||
type seedFile struct {
|
type seedFile struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Encoding string `json:"encoding,omitempty"`
|
||||||
Mode int `json:"mode,omitempty"`
|
Mode int `json:"mode,omitempty"`
|
||||||
}
|
}
|
||||||
seed := make([]seedFile, len(files))
|
seed := make([]seedFile, len(files))
|
||||||
for i, file := range files {
|
for i, file := range files {
|
||||||
seed[i] = seedFile{Path: file.Path, Content: file.Content, Mode: file.Mode}
|
seed[i] = seedFile{Path: file.Path, Content: file.Content, Encoding: file.Encoding, Mode: file.Mode}
|
||||||
}
|
}
|
||||||
body, err := json.Marshal(seed)
|
body, err := json.Marshal(seed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
|||||||
plugin.LifecycleAssets = []domain.PluginAssetFile{
|
plugin.LifecycleAssets = []domain.PluginAssetFile{
|
||||||
{Path: "actions/install.json", Content: `{"version":1,"action":"install","mode":"oneshot"}`, Mode: 0o600},
|
{Path: "actions/install.json", Content: `{"version":1,"action":"install","mode":"oneshot"}`, Mode: 0o600},
|
||||||
{Path: "bin/install-server", Content: "#!/usr/bin/env sh\n", Mode: 0o700},
|
{Path: "bin/install-server", Content: "#!/usr/bin/env sh\n", Mode: 0o700},
|
||||||
|
{Path: "assets/map.bin", Content: base64.StdEncoding.EncodeToString([]byte{0xff, 0x00, 0x7f}), Encoding: "base64", Mode: 0o600},
|
||||||
}
|
}
|
||||||
plugin.RuntimeProfiles.LogSources = append(plugin.RuntimeProfiles.LogSources,
|
plugin.RuntimeProfiles.LogSources = append(plugin.RuntimeProfiles.LogSources,
|
||||||
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
||||||
@@ -135,10 +136,10 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
|||||||
if err := json.Unmarshal(decodedSeed, &seedFiles); err != nil {
|
if err := json.Unmarshal(decodedSeed, &seedFiles); err != nil {
|
||||||
t.Fatalf("unmarshal workspace seed: %v", err)
|
t.Fatalf("unmarshal workspace seed: %v", err)
|
||||||
}
|
}
|
||||||
if platformInput.ProfileKey != "local" || len(seedFiles) != 3 || seedFiles[1].Path != "bin/install-server" || seedFiles[1].Content == "" {
|
if platformInput.ProfileKey != "local" || len(seedFiles) != 4 || seedFiles[1].Path != "bin/install-server" || seedFiles[1].Content == "" || seedFiles[2].Encoding != "base64" {
|
||||||
t.Fatalf("platform builder received incomplete plugin workspace seed: profile=%q seed=%+v", platformInput.ProfileKey, seedFiles)
|
t.Fatalf("platform builder received incomplete plugin workspace seed: profile=%q seed=%+v", platformInput.ProfileKey, seedFiles)
|
||||||
}
|
}
|
||||||
if seedFiles[2].Path != ".platform/autonomous-lifecycle-plan.json" || seedFiles[2].Content == "" || seedFiles[2].Mode != 0o600 {
|
if seedFiles[3].Path != ".platform/autonomous-lifecycle-plan.json" || seedFiles[3].Content == "" || seedFiles[3].Mode != 0o600 {
|
||||||
t.Fatalf("workspace seed did not include autonomous lifecycle plan file: %+v", seedFiles)
|
t.Fatalf("workspace seed did not include autonomous lifecycle plan file: %+v", seedFiles)
|
||||||
}
|
}
|
||||||
plan := platformInput.AutonomousLifecycle
|
plan := platformInput.AutonomousLifecycle
|
||||||
@@ -152,7 +153,7 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
|||||||
t.Fatalf("autonomous lifecycle plan must not carry file-tail sources into process.start: %+v", plan.LogSources)
|
t.Fatalf("autonomous lifecycle plan must not carry file-tail sources into process.start: %+v", plan.LogSources)
|
||||||
}
|
}
|
||||||
var seededPlan domain.RunAutonomousLifecyclePlan
|
var seededPlan domain.RunAutonomousLifecyclePlan
|
||||||
if err := json.Unmarshal([]byte(seedFiles[2].Content), &seededPlan); err != nil {
|
if err := json.Unmarshal([]byte(seedFiles[3].Content), &seededPlan); err != nil {
|
||||||
t.Fatalf("unmarshal seeded autonomous lifecycle plan: %v", err)
|
t.Fatalf("unmarshal seeded autonomous lifecycle plan: %v", err)
|
||||||
}
|
}
|
||||||
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey {
|
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey {
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ServerRuntimeActions{}, err
|
return domain.ServerRuntimeActions{}, err
|
||||||
}
|
}
|
||||||
endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, endpointErr := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
runRegistered := endpointErr == nil
|
runRegistered := endpointErr == nil
|
||||||
if endpointErr != nil && !errors.Is(endpointErr, repo.ErrNotFound) {
|
if endpointErr != nil && !errors.Is(endpointErr, repo.ErrNotFound) {
|
||||||
return domain.ServerRuntimeActions{}, endpointErr
|
return domain.ServerRuntimeActions{}, endpointErr
|
||||||
@@ -514,7 +514,7 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
|||||||
if request.Checksum != artifact.Checksum {
|
if request.Checksum != artifact.Checksum {
|
||||||
return domain.RunUpdateJob{}, validationError("checksum must match artifact")
|
return domain.RunUpdateJob{}, validationError("checksum must match artifact")
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.RunUpdateJob{}, err
|
return domain.RunUpdateJob{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,10 +199,25 @@ func TestCoreServiceDistributionBuildIgnoresStaleRunEndpoint(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get runtime actions: %v", err)
|
t.Fatalf("get runtime actions: %v", err)
|
||||||
}
|
}
|
||||||
|
if actions.RunStatus != domain.RunEndpointStatusOffline {
|
||||||
|
t.Fatalf("stale Run endpoint must project offline runtime status, got %+v", actions)
|
||||||
|
}
|
||||||
|
staleRunGated := map[string]bool{"dependencies-check": false, "dependencies-install": false, "live-logs": false}
|
||||||
for _, action := range actions.Actions {
|
for _, action := range actions.Actions {
|
||||||
if action.Key == "generate-run" && !action.Available {
|
if action.Key == "generate-run" && !action.Available {
|
||||||
t.Fatalf("stale Run endpoint must not gate platform build availability: %+v", action)
|
t.Fatalf("stale Run endpoint must not gate platform build availability: %+v", action)
|
||||||
}
|
}
|
||||||
|
if _, gated := staleRunGated[action.Key]; gated {
|
||||||
|
staleRunGated[action.Key] = true
|
||||||
|
if action.Available || action.Reason != "Run heartbeat has not been observed" {
|
||||||
|
t.Fatalf("stale Run endpoint must gate runtime action %s, got %+v", action.Key, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, seen := range staleRunGated {
|
||||||
|
if !seen {
|
||||||
|
t.Fatalf("expected stale Run gating action %s in %+v", key, actions.Actions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func (svc *CoreService) RunPluginLifecycleForSession(sessionID string, request d
|
|||||||
if !containsString(plugin.ProductionLifecycle.Operations, string(request.Operation)) {
|
if !containsString(plugin.ProductionLifecycle.Operations, string(request.Operation)) {
|
||||||
return domain.PluginLifecycleResult{}, validationError("plugin lifecycle operation is not declared by the manifest")
|
return domain.PluginLifecycleResult{}, validationError("plugin lifecycle operation is not declared by the manifest")
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.PluginLifecycleResult{}, err
|
return domain.PluginLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func (svc *CoreService) ListRemoteAdapterDeclarationsForSession(sessionID string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request
|
|||||||
}
|
}
|
||||||
if selected.Key == "" {
|
if selected.Key == "" {
|
||||||
plugin, pluginErr := svc.store.GamePlugins().Get(instance.PluginID)
|
plugin, pluginErr := svc.store.GamePlugins().Get(instance.PluginID)
|
||||||
endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, endpointErr := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if pluginErr == nil && endpointErr == nil && plugin.Permissions.RemoteAccess && containsString(plugin.RemoteAccess.RunCapabilities, request.Capability) && containsString(endpoint.Capabilities, request.Capability) {
|
if pluginErr == nil && endpointErr == nil && plugin.Permissions.RemoteAccess && containsString(plugin.RemoteAccess.RunCapabilities, request.Capability) && containsString(endpoint.Capabilities, request.Capability) {
|
||||||
selected = domain.RemoteAdapterDeclaration{Key: "legacy-" + string(remoteAdapterKindForCapability(request.Capability)), Kind: remoteAdapterKindForCapability(request.Capability), TargetKeys: []string{request.TargetKey}, Capabilities: []string{request.Capability}, TimeoutSeconds: 30, MaxAttempts: 3}
|
selected = domain.RemoteAdapterDeclaration{Key: "legacy-" + string(remoteAdapterKindForCapability(request.Capability)), Kind: remoteAdapterKindForCapability(request.Capability), TargetKeys: []string{request.TargetKey}, Capabilities: []string{request.Capability}, TimeoutSeconds: 30, MaxAttempts: 3}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -739,17 +739,67 @@ func (svc *CoreService) RegisterGamePluginManifest(registration domain.GamePlugi
|
|||||||
if err := validator.ValidateGamePlugin(plugin); err != nil {
|
if err := validator.ValidateGamePlugin(plugin); err != nil {
|
||||||
return domain.GamePlugin{}, err
|
return domain.GamePlugin{}, err
|
||||||
}
|
}
|
||||||
if err := svc.store.GamePlugins().Create(plugin); err != nil {
|
if existing, err := svc.store.GamePlugins().Get(plugin.ID); err == nil {
|
||||||
if !errors.Is(err, repo.ErrDuplicate) {
|
|
||||||
return domain.GamePlugin{}, err
|
|
||||||
}
|
|
||||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||||
return domain.GamePlugin{}, err
|
return domain.GamePlugin{}, err
|
||||||
}
|
}
|
||||||
|
if err := svc.refreshServerPluginReferences(existing.ID, plugin.Version); err != nil {
|
||||||
|
return domain.GamePlugin{}, err
|
||||||
|
}
|
||||||
|
} else if errors.Is(err, repo.ErrNotFound) {
|
||||||
|
if err := svc.store.GamePlugins().Create(plugin); err != nil {
|
||||||
|
return domain.GamePlugin{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return domain.GamePlugin{}, err
|
||||||
}
|
}
|
||||||
return domain.CopyGamePlugin(plugin), nil
|
return domain.CopyGamePlugin(plugin), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshServerPluginReferences keeps existing server projections usable when a
|
||||||
|
// manifest is refreshed in place. The server and its logical runtime binding
|
||||||
|
// carry the manifest version used for lifecycle validation; leaving either at a
|
||||||
|
// stale version would make a healthy existing server impossible to start after
|
||||||
|
// the registry refresh.
|
||||||
|
func (svc *CoreService) refreshServerPluginReferences(pluginID, pluginVersion string) error {
|
||||||
|
if strings.TrimSpace(pluginID) == "" || strings.TrimSpace(pluginVersion) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
stamp := svc.now()
|
||||||
|
instances, err := svc.store.ServerInstances().List(domain.ServerInstanceFilter{PluginID: pluginID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, instance := range instances {
|
||||||
|
if instance.PluginVersion != pluginVersion {
|
||||||
|
instance.PluginVersion = pluginVersion
|
||||||
|
instance.UpdatedAt = stamp
|
||||||
|
if err := validator.ValidateStoredServerInstance(instance); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings, err := svc.store.RuntimeBindings().List(domain.RuntimeBindingFilter{ServerInstanceID: instance.ID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, binding := range bindings {
|
||||||
|
if binding.PluginID != pluginID || binding.PluginVersion == pluginVersion {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
binding.PluginVersion = pluginVersion
|
||||||
|
binding.UpdatedAt = stamp
|
||||||
|
if err := svc.store.RuntimeBindings().Update(binding); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRegistration) domain.GamePlugin {
|
func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRegistration) domain.GamePlugin {
|
||||||
registration = domain.CopyGamePluginManifestRegistration(registration)
|
registration = domain.CopyGamePluginManifestRegistration(registration)
|
||||||
manifest := registration.Manifest
|
manifest := registration.Manifest
|
||||||
@@ -1633,7 +1683,7 @@ func (svc *CoreService) CreateServerInstance(instance domain.ServerInstance) (do
|
|||||||
}
|
}
|
||||||
var endpoint domain.RunEndpoint
|
var endpoint domain.RunEndpoint
|
||||||
if strings.TrimSpace(instance.RunEndpointID) != "" {
|
if strings.TrimSpace(instance.RunEndpointID) != "" {
|
||||||
endpoint, err = svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err = svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ServerInstance{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
return domain.ServerInstance{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1779,7 +1829,7 @@ func (svc *CoreService) GetPlatformResourceUsage() (domain.PlatformResourceUsage
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.PlatformResourceUsage{}, err
|
return domain.PlatformResourceUsage{}, err
|
||||||
}
|
}
|
||||||
endpoints, err := svc.store.RunEndpoints().List(domain.RunEndpointFilter{})
|
endpoints, err := svc.ListRunEndpoints(domain.RunEndpointFilter{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.PlatformResourceUsage{}, err
|
return domain.PlatformResourceUsage{}, err
|
||||||
}
|
}
|
||||||
@@ -2212,7 +2262,7 @@ func (svc *CoreService) latestMetricsForServer(instance domain.ServerInstance) d
|
|||||||
}
|
}
|
||||||
return domain.ServerMetrics{
|
return domain.ServerMetrics{
|
||||||
ServerInstanceID: instance.ID,
|
ServerInstanceID: instance.ID,
|
||||||
Online: instance.State == domain.ServerInstanceStateRunning,
|
Online: false,
|
||||||
Source: "run-metrics-pending",
|
Source: "run-metrics-pending",
|
||||||
CollectedAt: svc.now(),
|
CollectedAt: svc.now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -165,6 +166,17 @@ func TestCoreServiceCreateListGetWorkflows(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCoreServiceRejectsServerCreationOnStaleRunEndpoint(t *testing.T) {
|
||||||
|
svc := newTestCoreService()
|
||||||
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||||
|
svc.now = func() time.Time { return fixedTime.Add(runHeartbeatStaleAfter + time.Second) }
|
||||||
|
|
||||||
|
_, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-stale-run", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Stale Run Server"})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "get run endpoint dependency") || !errors.Is(err, repo.ErrNotFound) {
|
||||||
|
t.Fatalf("expected stale Run endpoint to be rejected before binding, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCoreServiceCreateRemoteProgramJobCreatesManagementLogStreams(t *testing.T) {
|
func TestCoreServiceCreateRemoteProgramJobCreatesManagementLogStreams(t *testing.T) {
|
||||||
svc := newTestCoreService()
|
svc := newTestCoreService()
|
||||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||||
@@ -685,7 +697,7 @@ func TestCoreServiceMetricsAndConfigReadAreRoleScoped(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("list owner metrics: %v", err)
|
t.Fatalf("list owner metrics: %v", err)
|
||||||
}
|
}
|
||||||
if len(ownerMetrics) != 1 || ownerMetrics[0].ServerInstanceID != instance.ID || !ownerMetrics[0].Online || ownerMetrics[0].CPUPercent != nil || ownerMetrics[0].Source != "run-metrics-pending" {
|
if len(ownerMetrics) != 1 || ownerMetrics[0].ServerInstanceID != instance.ID || ownerMetrics[0].Online || ownerMetrics[0].CPUPercent != nil || ownerMetrics[0].Source != "run-metrics-pending" {
|
||||||
t.Fatalf("expected pending metrics without fabricated resource values, got %+v", ownerMetrics)
|
t.Fatalf("expected pending metrics without fabricated resource values, got %+v", ownerMetrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,8 +1235,8 @@ func TestCoreServiceRegistersGamePluginManifest(t *testing.T) {
|
|||||||
svc := newTestCoreService()
|
svc := newTestCoreService()
|
||||||
|
|
||||||
registration := validPluginManifestRegistration()
|
registration := validPluginManifestRegistration()
|
||||||
registration.Manifest.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Mode: 0o600}, {Path: "bin/install-server", Mode: 0o700}}
|
registration.Manifest.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Mode: 0o600}, {Path: "bin/install-server", Mode: 0o700}, {Path: "assets/map.bin", Mode: 0o600}}
|
||||||
registration.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Content: "{}", Mode: 0o600}, {Path: "bin/install-server", Content: "#!/usr/bin/env sh\n"}}
|
registration.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Content: "{}", Mode: 0o600}, {Path: "bin/install-server", Content: "#!/usr/bin/env sh\n"}, {Path: "assets/map.bin", Content: base64.StdEncoding.EncodeToString([]byte{0xff, 0x00, 0x7f}), Encoding: "base64"}}
|
||||||
plugin, err := svc.RegisterGamePluginManifest(registration)
|
plugin, err := svc.RegisterGamePluginManifest(registration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("register manifest: %v", err)
|
t.Fatalf("register manifest: %v", err)
|
||||||
@@ -1247,7 +1259,7 @@ func TestCoreServiceRegistersGamePluginManifest(t *testing.T) {
|
|||||||
if len(plugin.BridgeActions) != 4 || plugin.BridgeActions[0] != string(domain.PluginBridgeActionServerInstancesRead) {
|
if len(plugin.BridgeActions) != 4 || plugin.BridgeActions[0] != string(domain.PluginBridgeActionServerInstancesRead) {
|
||||||
t.Fatalf("expected bridge actions, got %+v", plugin.BridgeActions)
|
t.Fatalf("expected bridge actions, got %+v", plugin.BridgeActions)
|
||||||
}
|
}
|
||||||
if len(plugin.LifecycleAssets) != 2 || plugin.LifecycleAssets[1].Path != "bin/install-server" || plugin.LifecycleAssets[1].Mode != 0o700 {
|
if len(plugin.LifecycleAssets) != 3 || plugin.LifecycleAssets[1].Path != "bin/install-server" || plugin.LifecycleAssets[1].Mode != 0o700 || plugin.LifecycleAssets[2].Encoding != "base64" {
|
||||||
t.Fatalf("expected declared lifecycle assets with manifest mode defaults, got %+v", plugin.LifecycleAssets)
|
t.Fatalf("expected declared lifecycle assets with manifest mode defaults, got %+v", plugin.LifecycleAssets)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1624,6 +1636,12 @@ func TestCoreServiceUpsertsDuplicateGamePluginManifest(t *testing.T) {
|
|||||||
if _, err := svc.RegisterGamePluginManifest(registration); err != nil {
|
if _, err := svc.RegisterGamePluginManifest(registration); err != nil {
|
||||||
t.Fatalf("register first manifest: %v", err)
|
t.Fatalf("register first manifest: %v", err)
|
||||||
}
|
}
|
||||||
|
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "manifest-refresh-server", PluginID: registration.Manifest.ID, PluginVersion: registration.Manifest.Version, Name: "Manifest refresh server"}); err != nil {
|
||||||
|
t.Fatalf("create server for manifest refresh: %v", err)
|
||||||
|
}
|
||||||
|
if err := svc.store.RuntimeBindings().Create(domain.RuntimeBinding{ID: "runtime-binding-manifest-refresh-server", ServerInstanceID: "manifest-refresh-server", PluginID: registration.Manifest.ID, PluginVersion: registration.Manifest.Version, ProfileKey: "local", Mode: "local-process", Status: domain.RuntimeBindingStatusComplete, CreatedAt: fixedTime, UpdatedAt: fixedTime}); err != nil {
|
||||||
|
t.Fatalf("create runtime binding for manifest refresh: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
registration.Manifest.Version = "0.1.1"
|
registration.Manifest.Version = "0.1.1"
|
||||||
registration.Manifest.Description = "Development plugin refreshed"
|
registration.Manifest.Description = "Development plugin refreshed"
|
||||||
@@ -1639,6 +1657,14 @@ func TestCoreServiceUpsertsDuplicateGamePluginManifest(t *testing.T) {
|
|||||||
if err != nil || len(listed) != 1 || listed[0].ID != "game.example" || listed[0].Version != "0.1.1" {
|
if err != nil || len(listed) != 1 || listed[0].ID != "game.example" || listed[0].Version != "0.1.1" {
|
||||||
t.Fatalf("expected one refreshed plugin after upsert, listed=%+v err=%v", listed, err)
|
t.Fatalf("expected one refreshed plugin after upsert, listed=%+v err=%v", listed, err)
|
||||||
}
|
}
|
||||||
|
server, err := svc.GetServerInstance("manifest-refresh-server")
|
||||||
|
if err != nil || server.PluginVersion != "0.1.1" {
|
||||||
|
t.Fatalf("expected existing server to follow refreshed plugin version, server=%+v err=%v", server, err)
|
||||||
|
}
|
||||||
|
binding, err := svc.store.RuntimeBindings().Get("runtime-binding-manifest-refresh-server")
|
||||||
|
if err != nil || binding.PluginVersion != "0.1.1" {
|
||||||
|
t.Fatalf("expected runtime binding to follow refreshed plugin version, binding=%+v err=%v", binding, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoreServiceRejectsUnsafeGamePluginManifest(t *testing.T) {
|
func TestCoreServiceRejectsUnsafeGamePluginManifest(t *testing.T) {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (svc *CoreService) deployServerInstance(command domain.ServerLifecycleComma
|
|||||||
if strings.TrimSpace(instance.RunEndpointID) == "" {
|
if strings.TrimSpace(instance.RunEndpointID) == "" {
|
||||||
return domain.ServerLifecycleResult{}, validationError("an active Run heartbeat is required for legacy manual deployment dispatch")
|
return domain.ServerLifecycleResult{}, validationError("an active Run heartbeat is required for legacy manual deployment dispatch")
|
||||||
}
|
}
|
||||||
if _, err := svc.store.RunEndpoints().Get(instance.RunEndpointID); err != nil {
|
if _, err := svc.GetRunEndpoint(instance.RunEndpointID); err != nil {
|
||||||
return domain.ServerLifecycleResult{}, err
|
return domain.ServerLifecycleResult{}, err
|
||||||
}
|
}
|
||||||
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
|
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
|||||||
instance.ConfigChecksum = validator.BytesChecksum([]byte(instance.ConfigContent))
|
instance.ConfigChecksum = validator.BytesChecksum([]byte(instance.ConfigContent))
|
||||||
instance.ConfigUpdatedAt = stamp
|
instance.ConfigUpdatedAt = stamp
|
||||||
if strings.TrimSpace(create.DeploymentTargetID) != "" {
|
if strings.TrimSpace(create.DeploymentTargetID) != "" {
|
||||||
target, targetErr := svc.store.RunEndpoints().Get(create.DeploymentTargetID)
|
target, targetErr := svc.GetRunEndpoint(create.DeploymentTargetID)
|
||||||
if targetErr != nil {
|
if targetErr != nil {
|
||||||
return domain.ServerLifecycleResult{}, fmt.Errorf("get deployment target dependency: %w", targetErr)
|
return domain.ServerLifecycleResult{}, fmt.Errorf("get deployment target dependency: %w", targetErr)
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
|||||||
}
|
}
|
||||||
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance}), nil
|
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance}), nil
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(create.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(create.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ServerLifecycleResult{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
return domain.ServerLifecycleResult{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
||||||
}
|
}
|
||||||
@@ -296,7 +296,7 @@ func (svc *CoreService) lifecycleDependencies(pluginID string, runEndpointID str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get plugin dependency: %w", err)
|
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get plugin dependency: %w", err)
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(runEndpointID)
|
endpoint, err := svc.GetRunEndpoint(runEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get run endpoint dependency: %w", err)
|
||||||
}
|
}
|
||||||
@@ -335,7 +335,7 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
|
|||||||
var dllExtensions []domain.RuntimeDLLExtensionPlan
|
var dllExtensions []domain.RuntimeDLLExtensionPlan
|
||||||
var logSources []domain.RuntimeLogSource
|
var logSources []domain.RuntimeLogSource
|
||||||
if action == domain.ServerLifecycleActionStart && hasProfile {
|
if action == domain.ServerLifecycleActionStart && hasProfile {
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Job{}, err
|
return domain.Job{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func (svc *CoreService) resolveSourceRCONDispatch(instance domain.ServerInstance
|
|||||||
if plugin.Status != domain.GamePluginStatusInstalled || plugin.Version != instance.PluginVersion || !plugin.Permissions.RemoteAccess || !plugin.RemoteAccess.RCON || !containsString(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) || !containsString(plugin.RemoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) {
|
if plugin.Status != domain.GamePluginStatusInstalled || plugin.Version != instance.PluginVersion || !plugin.Permissions.RemoteAccess || !plugin.RemoteAccess.RCON || !containsString(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) || !containsString(plugin.RemoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) {
|
||||||
return sourceRCONDispatchResolution{}, forbiddenError("plugin does not declare SCUM RCON command access")
|
return sourceRCONDispatchResolution{}, forbiddenError("plugin does not declare SCUM RCON command access")
|
||||||
}
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sourceRCONDispatchResolution{}, err
|
return sourceRCONDispatchResolution{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package validator
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"browser.local/platform/domain"
|
"browser.local/platform/domain"
|
||||||
)
|
)
|
||||||
@@ -260,6 +263,9 @@ func validatePluginAssetFileDeclarations(prefix string, files []domain.PluginAss
|
|||||||
if file.Content != "" {
|
if file.Content != "" {
|
||||||
violations = append(violations, field+".content must be supplied only in registration assetFiles")
|
violations = append(violations, field+".content must be supplied only in registration assetFiles")
|
||||||
}
|
}
|
||||||
|
if file.Encoding != "" {
|
||||||
|
violations = append(violations, field+".encoding must be supplied only in registration assetFiles")
|
||||||
|
}
|
||||||
if file.Mode != 0 && file.Mode != 0o600 && file.Mode != 0o700 {
|
if file.Mode != 0 && file.Mode != 0o600 && file.Mode != 0o700 {
|
||||||
violations = append(violations, field+".mode is unsafe")
|
violations = append(violations, field+".mode is unsafe")
|
||||||
}
|
}
|
||||||
@@ -282,7 +288,10 @@ func validatePluginAssetFiles(prefix string, files []domain.PluginAssetFile) []s
|
|||||||
violations = append(violations, field+".path is duplicated")
|
violations = append(violations, field+".path is duplicated")
|
||||||
}
|
}
|
||||||
seen[file.Path] = struct{}{}
|
seen[file.Path] = struct{}{}
|
||||||
if len([]byte(file.Content)) > 64*1024 || strings.ContainsRune(file.Content, '\x00') || containsUnsafeRuntimeSecret(file.Content) {
|
content, err := pluginAssetContentBytes(file)
|
||||||
|
if err != nil {
|
||||||
|
violations = append(violations, field+err.Error())
|
||||||
|
} else if len(content) > 64*1024 || pluginAssetContentUnsafe(file, content) {
|
||||||
violations = append(violations, field+".content is unsafe")
|
violations = append(violations, field+".content is unsafe")
|
||||||
}
|
}
|
||||||
if file.Mode != 0 && (file.Mode < 0o400 || file.Mode > 0o700 || file.Mode&0o022 != 0) {
|
if file.Mode != 0 && (file.Mode < 0o400 || file.Mode > 0o700 || file.Mode&0o022 != 0) {
|
||||||
@@ -292,6 +301,31 @@ func validatePluginAssetFiles(prefix string, files []domain.PluginAssetFile) []s
|
|||||||
return violations
|
return violations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pluginAssetContentBytes(file domain.PluginAssetFile) ([]byte, error) {
|
||||||
|
switch strings.TrimSpace(file.Encoding) {
|
||||||
|
case "":
|
||||||
|
return []byte(file.Content), nil
|
||||||
|
case "base64":
|
||||||
|
content, err := base64.StdEncoding.DecodeString(strings.TrimSpace(file.Content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(".content is not valid base64")
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf(".encoding is unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginAssetContentUnsafe(file domain.PluginAssetFile, content []byte) bool {
|
||||||
|
if strings.TrimSpace(file.Encoding) == "base64" {
|
||||||
|
if bytes.ContainsRune(content, '\x00') || !utf8.Valid(content) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return containsUnsafeRuntimeSecret(string(content))
|
||||||
|
}
|
||||||
|
return bytes.ContainsRune(content, '\x00') || containsUnsafeRuntimeSecret(string(content))
|
||||||
|
}
|
||||||
|
|
||||||
func validateRegistrationAssetCoverage(declared []domain.PluginAssetFile, payload []domain.PluginAssetFile) []string {
|
func validateRegistrationAssetCoverage(declared []domain.PluginAssetFile, payload []domain.PluginAssetFile) []string {
|
||||||
if len(declared) == 0 {
|
if len(declared) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package validator
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -79,10 +80,12 @@ func TestValidateGamePluginManifestRegistrationValidatesAssetFileCoverage(t *tes
|
|||||||
registration.Manifest.AssetFiles = []domain.PluginAssetFile{
|
registration.Manifest.AssetFiles = []domain.PluginAssetFile{
|
||||||
{Path: "actions/install.json", Mode: 0o600},
|
{Path: "actions/install.json", Mode: 0o600},
|
||||||
{Path: "bin/install-server", Mode: 0o700},
|
{Path: "bin/install-server", Mode: 0o700},
|
||||||
|
{Path: "assets/map.bin", Mode: 0o600},
|
||||||
}
|
}
|
||||||
registration.AssetFiles = []domain.PluginAssetFile{
|
registration.AssetFiles = []domain.PluginAssetFile{
|
||||||
{Path: "actions/install.json", Content: "{}", Mode: 0o600},
|
{Path: "actions/install.json", Content: "{}", Mode: 0o600},
|
||||||
{Path: "bin/install-server", Content: "#!/usr/bin/env sh\n", Mode: 0o700},
|
{Path: "bin/install-server", Content: "#!/usr/bin/env sh\n", Mode: 0o700},
|
||||||
|
{Path: "assets/map.bin", Content: base64.StdEncoding.EncodeToString([]byte{0xff, 0x00, 0x7f}), Encoding: "base64", Mode: 0o600},
|
||||||
}
|
}
|
||||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||||
t.Fatalf("expected declared asset files with matching content to validate, got %v", err)
|
t.Fatalf("expected declared asset files with matching content to validate, got %v", err)
|
||||||
@@ -105,6 +108,12 @@ func TestValidateGamePluginManifestRegistrationValidatesAssetFileCoverage(t *tes
|
|||||||
if err := ValidateGamePluginManifestRegistration(inlineContent); err == nil || !strings.Contains(err.Error(), "content must be supplied only") {
|
if err := ValidateGamePluginManifestRegistration(inlineContent); err == nil || !strings.Contains(err.Error(), "content must be supplied only") {
|
||||||
t.Fatalf("expected manifest inline content rejection, got %v", err)
|
t.Fatalf("expected manifest inline content rejection, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafeEncodedText := domain.CopyGamePluginManifestRegistration(registration)
|
||||||
|
unsafeEncodedText.AssetFiles[2].Content = base64.StdEncoding.EncodeToString([]byte("password=secret"))
|
||||||
|
if err := ValidateGamePluginManifestRegistration(unsafeEncodedText); err == nil || !strings.Contains(err.Error(), "content is unsafe") {
|
||||||
|
t.Fatalf("expected unsafe base64 text rejection, got %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateGamePluginManifestRegistrationValidatesRuntimeProfiles(t *testing.T) {
|
func TestValidateGamePluginManifestRegistrationValidatesRuntimeProfiles(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -9,6 +9,6 @@ Use these root-level commands for day-to-day work:
|
|||||||
- `scripts/dev-reset.sh`: stop the stack and delete only the safe local debug data root.
|
- `scripts/dev-reset.sh`: stop the stack and delete only the safe local debug data root.
|
||||||
- `scripts/browser-acceptance.sh`: run the full self-starting browser acceptance suite.
|
- `scripts/browser-acceptance.sh`: run the full self-starting browser acceptance suite.
|
||||||
- `scripts/check-structure.sh`: verify required repository structure.
|
- `scripts/check-structure.sh`: verify required repository structure.
|
||||||
- `scripts/check-all.sh`: run structure, backend, frontend, and plugin checks.
|
- `scripts/check-all.sh`: run structure, backend, frontend, plugin checks, and the independent Run checkout tests when `RUN_SOURCE_DIR` or `run/` is available.
|
||||||
|
|
||||||
Implementation details for the managed local stack live under `scripts/local-debug/`. Prefer the root commands above instead of calling those internals directly.
|
Implementation details for the managed local stack live under `scripts/local-debug/`. Prefer the root commands above instead of calling those internals directly.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
RUN_SOURCE_DIR="${RUN_SOURCE_DIR:-${RUN_REPO_DIR:-$ROOT_DIR/run}}"
|
||||||
|
|
||||||
ensure_node_deps() {
|
ensure_node_deps() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
@@ -14,6 +15,12 @@ ensure_node_deps() {
|
|||||||
|
|
||||||
(cd "$ROOT_DIR/platform" && go test ./...)
|
(cd "$ROOT_DIR/platform" && go test ./...)
|
||||||
|
|
||||||
|
if [[ -d "$RUN_SOURCE_DIR/.git" ]]; then
|
||||||
|
(cd "$RUN_SOURCE_DIR" && go test ./...)
|
||||||
|
else
|
||||||
|
printf 'independent run checkout not found at %s; skipping run checks\n' "$RUN_SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
ensure_node_deps "$ROOT_DIR/platform_web"
|
ensure_node_deps "$ROOT_DIR/platform_web"
|
||||||
(cd "$ROOT_DIR/platform_web" && npm run typecheck && npm run test && npm run build)
|
(cd "$ROOT_DIR/platform_web" && npm run typecheck && npm run test && npm run build)
|
||||||
|
|
||||||
|
|||||||
@@ -378,6 +378,23 @@ create_server_workflow() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create_server_instance() {
|
||||||
|
local label="$1"
|
||||||
|
local server_id="$2"
|
||||||
|
local request_file="$3"
|
||||||
|
local response_file="$4"
|
||||||
|
if json_post "$API_URL/server-instances" "$request_file" "$response_file" "${AUTH_HEADER[@]}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -s "$response_file" ]] && response_code_is_duplicate "$response_file"; then
|
||||||
|
json_get "$API_URL/server-instances/$server_id" "$response_file" "${AUTH_HEADER[@]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s server creation failed; platform response:\n' "$label" >&2
|
||||||
|
sed -n '1,160p' "$response_file" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
require_file_contains() {
|
require_file_contains() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local pattern="$2"
|
local pattern="$2"
|
||||||
@@ -970,9 +987,14 @@ function readAssetFiles(manifest) {
|
|||||||
return (manifest.assetFiles ?? []).map((file) => ({
|
return (manifest.assetFiles ?? []).map((file) => ({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
mode: file.mode,
|
mode: file.mode,
|
||||||
content: fs.readFileSync(path.join(manifestDir, file.path), "utf8")
|
...readAssetFileContent(path.join(manifestDir, file.path), file.path)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
function readAssetFileContent(assetPath, logicalPath) {
|
||||||
|
const body = fs.readFileSync(assetPath);
|
||||||
|
if (/\.(?:json|cmd|sh|sql|txt|ya?ml)$/i.test(logicalPath)) return { content: body.toString("utf8") };
|
||||||
|
return { content: body.toString("base64"), encoding: "base64" };
|
||||||
|
}
|
||||||
const manifest = {
|
const manifest = {
|
||||||
id: source.id,
|
id: source.id,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
@@ -1046,9 +1068,14 @@ function readAssetFiles(manifest) {
|
|||||||
return (manifest.assetFiles ?? []).map((file) => ({
|
return (manifest.assetFiles ?? []).map((file) => ({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
mode: file.mode,
|
mode: file.mode,
|
||||||
content: fs.readFileSync(path.join(manifestDir, file.path), "utf8")
|
...readAssetFileContent(path.join(manifestDir, file.path), file.path)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
function readAssetFileContent(assetPath, logicalPath) {
|
||||||
|
const body = fs.readFileSync(assetPath);
|
||||||
|
if (/\.(?:json|cmd|sh|sql|txt|ya?ml)$/i.test(logicalPath)) return { content: body.toString("utf8") };
|
||||||
|
return { content: body.toString("base64"), encoding: "base64" };
|
||||||
|
}
|
||||||
const localRunCapabilities = [
|
const localRunCapabilities = [
|
||||||
"process.install",
|
"process.install",
|
||||||
"process.start",
|
"process.start",
|
||||||
@@ -1149,8 +1176,8 @@ cat >"$WORK_DIR/create-log-session-server.request.json" <<JSON
|
|||||||
{
|
{
|
||||||
"id": "$LOG_SESSION_SERVER_ID",
|
"id": "$LOG_SESSION_SERVER_ID",
|
||||||
"pluginId": "game.example",
|
"pluginId": "game.example",
|
||||||
"name": "Current Log Session Smoke $SMOKE_INVOCATION_ID",
|
"runEndpointId": "$RUN_ENDPOINT_ID",
|
||||||
"idempotencyKey": "local-debug-log-session-create-$SMOKE_INVOCATION_ID"
|
"name": "Current Log Session Smoke $SMOKE_INVOCATION_ID"
|
||||||
}
|
}
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
@@ -1224,7 +1251,7 @@ create_server_workflow "dev" "$SERVER_LOCAL_ID" "$WORK_DIR/create-server.request
|
|||||||
reject_forbidden_fragments "$WORK_DIR/create-server.response.json"
|
reject_forbidden_fragments "$WORK_DIR/create-server.response.json"
|
||||||
|
|
||||||
printf 'creating current supervised log session fixture server\n'
|
printf 'creating current supervised log session fixture server\n'
|
||||||
create_server_workflow "log session fixture" "$LOG_SESSION_SERVER_ID" "$WORK_DIR/create-log-session-server.request.json" "$WORK_DIR/create-log-session-server.response.json"
|
create_server_instance "log session fixture" "$LOG_SESSION_SERVER_ID" "$WORK_DIR/create-log-session-server.request.json" "$WORK_DIR/create-log-session-server.response.json"
|
||||||
reject_forbidden_fragments "$WORK_DIR/create-log-session-server.response.json"
|
reject_forbidden_fragments "$WORK_DIR/create-log-session-server.response.json"
|
||||||
|
|
||||||
printf 'creating SCUM server lifecycle workflows through platform API\n'
|
printf 'creating SCUM server lifecycle workflows through platform API\n'
|
||||||
|
|||||||
@@ -60,6 +60,39 @@ wait_for_pid_exit() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hash_stream() {
|
||||||
|
shasum -a 256 | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
tracked_tree_fingerprint() {
|
||||||
|
local repo_dir="$1"
|
||||||
|
shift
|
||||||
|
(
|
||||||
|
cd "$repo_dir"
|
||||||
|
{
|
||||||
|
git ls-files -z -- "$@"
|
||||||
|
git ls-files -z --others --exclude-standard -- "$@"
|
||||||
|
} | sort -z | while IFS= read -r -d '' file; do
|
||||||
|
[[ -f "$file" ]] || continue
|
||||||
|
shasum -a 256 "$file"
|
||||||
|
done
|
||||||
|
git diff --binary -- "$@"
|
||||||
|
) | hash_stream
|
||||||
|
}
|
||||||
|
|
||||||
|
file_fingerprint() {
|
||||||
|
local file="$1"
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
printf 'missing:%s\n' "$file" | hash_stream
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
shasum -a 256 "$file" | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
combined_fingerprint() {
|
||||||
|
printf '%s\n' "$@" | hash_stream
|
||||||
|
}
|
||||||
|
|
||||||
descendant_pids() {
|
descendant_pids() {
|
||||||
local pid="$1"
|
local pid="$1"
|
||||||
local child
|
local child
|
||||||
@@ -148,17 +181,30 @@ ensure_port_available() {
|
|||||||
start_service() {
|
start_service() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local service_dir="$2"
|
local service_dir="$2"
|
||||||
|
local fingerprint="$3"
|
||||||
local pid_file="$LOCAL_DEBUG_PID_DIR/$name.pid"
|
local pid_file="$LOCAL_DEBUG_PID_DIR/$name.pid"
|
||||||
shift 2
|
local fingerprint_file="$LOCAL_DEBUG_PID_DIR/$name.fingerprint"
|
||||||
|
shift 3
|
||||||
|
|
||||||
if [[ -f "$pid_file" ]]; then
|
if [[ -f "$pid_file" ]]; then
|
||||||
local existing_pid
|
local existing_pid
|
||||||
existing_pid="$(cat "$pid_file")"
|
existing_pid="$(cat "$pid_file")"
|
||||||
if kill -0 "$existing_pid" 2>/dev/null; then
|
if kill -0 "$existing_pid" 2>/dev/null; then
|
||||||
|
local previous_fingerprint=""
|
||||||
|
if [[ -n "$fingerprint" && -f "$fingerprint_file" ]]; then
|
||||||
|
previous_fingerprint="$(cat "$fingerprint_file")"
|
||||||
|
fi
|
||||||
|
if [[ -n "$fingerprint" && "$previous_fingerprint" != "$fingerprint" ]]; then
|
||||||
|
printf '%s source or environment changed; restarting pid %s\n' "$name" "$existing_pid"
|
||||||
|
stop_pid_tree "$name" "$existing_pid"
|
||||||
|
rm -f "$pid_file"
|
||||||
|
else
|
||||||
printf '%s already running with pid %s\n' "$name" "$existing_pid"
|
printf '%s already running with pid %s\n' "$name" "$existing_pid"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
rm -f "$pid_file"
|
else
|
||||||
|
rm -f "$pid_file" "$fingerprint_file"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'starting %s\n' "$name"
|
printf 'starting %s\n' "$name"
|
||||||
@@ -167,6 +213,11 @@ start_service() {
|
|||||||
exec nohup "$@" >"$LOCAL_DEBUG_LOG_DIR/$name.log" 2>&1
|
exec nohup "$@" >"$LOCAL_DEBUG_LOG_DIR/$name.log" 2>&1
|
||||||
) >/dev/null 2>&1 &
|
) >/dev/null 2>&1 &
|
||||||
printf '%s' "$!" >"$pid_file"
|
printf '%s' "$!" >"$pid_file"
|
||||||
|
if [[ -n "$fingerprint" ]]; then
|
||||||
|
printf '%s' "$fingerprint" >"$fingerprint_file"
|
||||||
|
else
|
||||||
|
rm -f "$fingerprint_file"
|
||||||
|
fi
|
||||||
printf '%s pid %s log %s\n' "$name" "$(cat "$pid_file")" "$LOCAL_DEBUG_LOG_DIR/$name.log"
|
printf '%s pid %s log %s\n' "$name" "$(cat "$pid_file")" "$LOCAL_DEBUG_LOG_DIR/$name.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,10 +253,26 @@ printf 'platform log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform.log"
|
|||||||
printf 'run log: %s\n' "$LOCAL_DEBUG_LOG_DIR/run.log"
|
printf 'run log: %s\n' "$LOCAL_DEBUG_LOG_DIR/run.log"
|
||||||
printf 'platform_web log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform_web.log"
|
printf 'platform_web log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform_web.log"
|
||||||
|
|
||||||
|
platform_fingerprint="$(combined_fingerprint \
|
||||||
|
"$(tracked_tree_fingerprint "$ROOT_DIR" platform scripts/dev-start.sh scripts/local-debug)" \
|
||||||
|
"PLATFORM_ADDR=$PLATFORM_ADDR" \
|
||||||
|
"PLATFORM_STORAGE_BACKEND=$PLATFORM_STORAGE_BACKEND" \
|
||||||
|
"PLATFORM_METADATA_PATH=$PLATFORM_METADATA_PATH" \
|
||||||
|
"PLATFORM_LOG_BODY_BACKEND=$PLATFORM_LOG_BODY_BACKEND" \
|
||||||
|
"PLATFORM_RUN_RELEASE_URL=$PLATFORM_RUN_RELEASE_URL" \
|
||||||
|
"PLATFORM_BUILDER_IMAGE=$PLATFORM_BUILDER_IMAGE" \
|
||||||
|
"PLATFORM_BUILDER_SOURCE_DIR=$PLATFORM_BUILDER_SOURCE_DIR" \
|
||||||
|
"PLATFORM_BUILDER_SOURCE_REVISION=$PLATFORM_BUILDER_SOURCE_REVISION")"
|
||||||
|
web_fingerprint="$(combined_fingerprint \
|
||||||
|
"$(tracked_tree_fingerprint "$ROOT_DIR" platform_web package.json package-lock.json scripts/dev-start.sh scripts/local-debug)" \
|
||||||
|
"PLATFORM_API_PROXY=$PLATFORM_API_PROXY" \
|
||||||
|
"VITE_PLATFORM_API_BASE_URL=$VITE_PLATFORM_API_BASE_URL" \
|
||||||
|
"VITE_ENABLE_LOCAL_AUTH_FALLBACK=$VITE_ENABLE_LOCAL_AUTH_FALLBACK")"
|
||||||
|
|
||||||
ensure_port_available platform "$LOCAL_DEBUG_PLATFORM_PORT"
|
ensure_port_available platform "$LOCAL_DEBUG_PLATFORM_PORT"
|
||||||
ensure_port_available platform_web "$LOCAL_DEBUG_WEB_PORT"
|
ensure_port_available platform_web "$LOCAL_DEBUG_WEB_PORT"
|
||||||
|
|
||||||
start_service platform "$ROOT_DIR/platform" env \
|
start_service platform "$ROOT_DIR/platform" "$platform_fingerprint" env \
|
||||||
GOCACHE="$GOCACHE" \
|
GOCACHE="$GOCACHE" \
|
||||||
PLATFORM_ADDR="$PLATFORM_ADDR" \
|
PLATFORM_ADDR="$PLATFORM_ADDR" \
|
||||||
PLATFORM_STORAGE_BACKEND="$PLATFORM_STORAGE_BACKEND" \
|
PLATFORM_STORAGE_BACKEND="$PLATFORM_STORAGE_BACKEND" \
|
||||||
@@ -230,7 +297,7 @@ start_service platform "$ROOT_DIR/platform" env \
|
|||||||
|
|
||||||
wait_for_url platform "$(local_debug_platform_url)/healthz"
|
wait_for_url platform "$(local_debug_platform_url)/healthz"
|
||||||
|
|
||||||
start_service platform_web "$ROOT_DIR" env \
|
start_service platform_web "$ROOT_DIR" "$web_fingerprint" env \
|
||||||
PLATFORM_API_PROXY="$PLATFORM_API_PROXY" \
|
PLATFORM_API_PROXY="$PLATFORM_API_PROXY" \
|
||||||
VITE_PLATFORM_API_BASE_URL="$VITE_PLATFORM_API_BASE_URL" \
|
VITE_PLATFORM_API_BASE_URL="$VITE_PLATFORM_API_BASE_URL" \
|
||||||
VITE_ENABLE_LOCAL_AUTH_FALLBACK="$VITE_ENABLE_LOCAL_AUTH_FALLBACK" \
|
VITE_ENABLE_LOCAL_AUTH_FALLBACK="$VITE_ENABLE_LOCAL_AUTH_FALLBACK" \
|
||||||
@@ -240,7 +307,22 @@ wait_for_url platform_web "$(local_debug_web_url)"
|
|||||||
|
|
||||||
stop_orphaned_run_bootstrap
|
stop_orphaned_run_bootstrap
|
||||||
local_debug_build_bootstrap_run
|
local_debug_build_bootstrap_run
|
||||||
start_service run "$(dirname "$RUN_BOOTSTRAP_BIN")" env \
|
run_fingerprint="$(combined_fingerprint \
|
||||||
|
"$(tracked_tree_fingerprint "$RUN_SOURCE_DIR" .)" \
|
||||||
|
"$(file_fingerprint "$RUN_BOOTSTRAP_BIN")" \
|
||||||
|
"RUN_MODE=$RUN_MODE" \
|
||||||
|
"RUN_PLATFORM_URL=$RUN_PLATFORM_URL" \
|
||||||
|
"RUN_ENDPOINT_ID=$RUN_ENDPOINT_ID" \
|
||||||
|
"RUN_DISPLAY_NAME=$RUN_DISPLAY_NAME" \
|
||||||
|
"RUN_VERSION=$RUN_VERSION" \
|
||||||
|
"RUN_WORKSPACE_ROOT=$RUN_WORKSPACE_ROOT" \
|
||||||
|
"RUN_BUILD_SOURCE_ROOT=$RUN_BUILD_SOURCE_ROOT" \
|
||||||
|
"RUN_SPOOL_ROOT=$RUN_SPOOL_ROOT" \
|
||||||
|
"RUN_MAX_JOBS=$RUN_MAX_JOBS" \
|
||||||
|
"RUN_HEARTBEAT_INTERVAL_MS=$RUN_HEARTBEAT_INTERVAL_MS" \
|
||||||
|
"RUN_POLL_INTERVAL_MS=$RUN_POLL_INTERVAL_MS" \
|
||||||
|
"RUN_RETRY_BACKOFF_MS=$RUN_RETRY_BACKOFF_MS")"
|
||||||
|
start_service run "$(dirname "$RUN_BOOTSTRAP_BIN")" "$run_fingerprint" env \
|
||||||
GOCACHE="$GOCACHE" \
|
GOCACHE="$GOCACHE" \
|
||||||
RUN_MODE="$RUN_MODE" \
|
RUN_MODE="$RUN_MODE" \
|
||||||
RUN_PLATFORM_URL="$RUN_PLATFORM_URL" \
|
RUN_PLATFORM_URL="$RUN_PLATFORM_URL" \
|
||||||
|
|||||||
Reference in New Issue
Block a user