Add run lifecycle report projection

This commit is contained in:
npc0-hue
2026-08-06 20:35:13 +08:00
parent 4e78957a60
commit 68921b3f68
13 changed files with 270 additions and 8 deletions
@@ -9,11 +9,13 @@ The corrected ownership model is: Platform builds and authenticates a server-sco
**Goals:**
- Stop generated Run registration from enqueueing `process.start` or `process.status` jobs as a bootstrap side effect.
- Add a generated-package autonomous lifecycle plan containing plugin lifecycle action refs, dependency declarations, safe deployment inputs, log sources, and selected runtime profile metadata.
- Implement the independent Run repository's generic plan loader/executor so generated packages consume `.platform/autonomous-lifecycle-plan.json` locally and report terminal lifecycle facts back to Platform.
- Add a Platform lifecycle report endpoint that authenticates the active Run session, validates the server binding, records audit evidence, and projects visible server state from Run-reported facts without creating jobs.
- Keep Platform responsible for server records, registration binding, auth, distribution builds, audit, and visible projections from Run reports.
- Update governance and protocol docs so future work treats Run as the lifecycle authority.
**Non-Goals:**
- Implement the independent `run` repository's plan executor in this repo.
- Re-add the independent `run` repository as a source tree owned by this repo.
- Add game-specific SCUM install/start behavior to Platform.
- Move distribution builds to machine-side Run endpoints or expose distribution-build authority to generated Runs.
- Remove explicit operator lifecycle command APIs in this change.
@@ -24,11 +26,12 @@ The corrected ownership model is: Platform builds and authenticates a server-sco
- **Use plugin declarations, not Platform logic, for game behavior.** The plan references action files, dependency probes/install plans, process log sources, DLL extension declarations, and sanitized deployment inputs already declared by the plugin. It does not include SCUM executable names, Steam app IDs, ports, or platform-side command synthesis.
- **Make registration binding-only for generated Run bootstrap.** `RegisterRunHello` continues to authenticate the component, upsert endpoint metadata, and issue a session token. It does not dispatch lifecycle or reconciliation work merely because a generated Run appeared.
- **Keep Platform projections report-driven.** Existing terminal job/result projection can remain for explicit lifecycle commands, but generated Run startup state must converge through Run reports rather than Platform's stale stored state or registration-time probes.
- **Report autonomous execution as observed facts, not job completions.** Run submits terminal autonomous lifecycle results through a signed lifecycle report route. Platform verifies the report belongs to the bound server/run session, then reuses lifecycle projection logic without manufacturing a platform job lease.
- **Preserve builder security boundaries.** The platform-owned builder receives the plaintext component auth key internally and the autonomous plan as build input. Machine-side run endpoints still cannot claim `distribution.build` jobs or fetch plaintext build input.
## Risks / Trade-offs
- **Run repository lag** -> The generated package can carry the plan before the independent Run executable consumes it. Mitigation: protocol and build input are explicit, and this repo does not re-add Run source.
- **Cross-repository release skew** -> Platform can build packages with the plan before every deployed Run binary has the autonomous executor. Mitigation: generated Run logs explicit plan load/skip/failure states, tests cover the independent `run` repository, and this repo still does not own the `run/` source tree.
- **Stored Platform state may look stale until Run reports** -> Registration no longer paper-over probes with Platform jobs. Mitigation: UI/API must treat persisted lifecycle state as projection, not observed process truth.
- **Plan drift between build and execution** -> A package carries the plugin declarations and deployment revision available at build time. Mitigation: include plugin version, profile key, deployment revision, and target release so Run and Platform can report stale-plan evidence.
- **Operator command APIs still dispatch jobs** -> This change fixes generated Run autonomous startup first. Explicit commands remain auditable Platform requests until a later change redesigns command transport around Run-owned intent handling.
@@ -38,6 +41,8 @@ The corrected ownership model is: Platform builds and authenticates a server-sco
- Stop queuing registration-time lifecycle/status jobs for generated Runs.
- Extend run distribution build input and DTOs with `autonomousLifecycle` for Run packages only.
- Update platform builder input materialization so the generated package has a serialized plan alongside the existing workspace seed.
- Add Platform's signed lifecycle report endpoint for Run-owned bootstrap results.
- Update the independent Run repository to load, validate, execute, and report the autonomous plan using generic lifecycle capabilities.
- Update service tests to assert registration does not enqueue bootstrap/reconciliation jobs and build input includes the plan.
## Open Questions
@@ -11,6 +11,10 @@ Generated Run packages SHALL carry a bounded autonomous lifecycle plan that lets
- **WHEN** the generated Run executable starts on its host
- **THEN** Run can read the embedded autonomous lifecycle plan and execute plugin-declared init, dependency verification/install, install-if-needed, readiness/status, and start behavior locally
#### Scenario: Generated Run reports autonomous bootstrap result
- **WHEN** Run completes an autonomous lifecycle bootstrap action from the embedded plan
- **THEN** Run reports the terminal lifecycle result to Platform without requiring a Platform job claim, acknowledgement, lease token, or job result
### Requirement: Platform follows Run-reported lifecycle facts
Platform SHALL treat persisted server lifecycle state as a projection of Run-reported lifecycle facts, heartbeats, logs, and terminal process reports rather than as authoritative observed process truth.
@@ -22,6 +26,10 @@ Platform SHALL treat persisted server lifecycle state as a projection of Run-rep
- **WHEN** Run reports that the managed process is running
- **THEN** Platform projects the server as running based on the Run report
#### Scenario: Run reports through the signed lifecycle report channel
- **WHEN** a registered Run submits a terminal lifecycle report for its bound server instance
- **THEN** Platform validates the active Run session and server binding, records audit evidence, and updates the visible server projection from the reported process facts
### Requirement: Plugins declare game-specific lifecycle behavior
Plugins SHALL declare lifecycle action refs, dependency probes, install plans, runtime profiles, log sources, and deployment templates needed by Run, and Platform SHALL NOT hardcode game-specific install, update, status, or startup behavior.
@@ -14,9 +14,16 @@
- [x] 2.1 Add autonomous lifecycle plan domain/build-input structures without exposing the plan through machine job-channel DTOs.
- [x] 2.2 Populate the plan from plugin lifecycle declarations, runtime profile data, dependency declarations, log sources, DLL extensions, and deployment definition.
- [x] 2.3 Stop generated Run registration from enqueueing bootstrap start or status reconciliation jobs.
- [x] 2.4 Update protocol and governance docs to make Run the lifecycle authority.
- [x] 2.4 Add a signed Platform lifecycle report endpoint that projects server state from Run-owned terminal lifecycle facts without creating jobs.
- [x] 2.5 Update protocol and governance docs to make Run the lifecycle authority.
## 3. Verification
## 3. Independent Run Implementation
- [x] 3.1 Update service tests for autonomous build input and no registration-time lifecycle dispatch.
- [x] 3.2 Run targeted Go tests plus repository structure checks.
- [x] 3.1 Add generic autonomous lifecycle plan protocol types and validation in the independent `run` repository.
- [x] 3.2 Load `.platform/autonomous-lifecycle-plan.json` from the generated package workspace, validate package identity/target, run dependency probes/install plans, execute the bootstrap action, and report the terminal lifecycle result to Platform.
- [x] 3.3 Keep Run generic: no SCUM executable names, Steam app IDs, install paths, ports, or game-specific branches.
## 4. Verification
- [x] 4.1 Update service/API/runtime tests for autonomous build input, no registration-time lifecycle dispatch, Run-owned bootstrap execution, lifecycle reporting, and signed report routing.
- [x] 4.2 Run full Go tests for `platform/` and the independent `run` repository plus repository structure checks.
+1
View File
@@ -142,6 +142,7 @@ func TestRunHTTPEnvelopeRequiresValidSignatureAndRejectsReplay(t *testing.T) {
assertErrorResponse(t, staleClaim, http.StatusUnauthorized, errorCodeUnauthorized)
privateUpdateBodies := map[string]any{
"/api/v1/run/lifecycle/report": dto.RunLifecycleReportRequest{RunEndpointID: "run-local", SessionToken: token, ServerInstanceID: "server-signed", Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: dto.JobProgressBody{Percent: 100}, ExecutionResult: dto.RunJobExecutionResultBody{Kind: "process", ProcessState: "running"}},
"/api/v1/run/jobs/dependency-input": dto.DependencyExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
"/api/v1/run/jobs/protected-request-input": dto.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1, FencingToken: 1},
"/api/v1/run/jobs/source-rcon-input": dto.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: token, JobID: "job-signed", LeaseToken: "lease", Attempt: 1},
+22
View File
@@ -42,6 +42,28 @@ func TestRunControlAPIHelloHeartbeatWorkflow(t *testing.T) {
}
}
func TestRunLifecycleReportAPIProjectsServerState(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
helloRequest := validRunControlHelloRequest()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, "logs.read")
helloRequest.CapabilityReport.Fingerprint = "cap-lifecycle-report"
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, helloRequest))
server := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-lifecycle-report-api", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Lifecycle Report API", State: domain.ServerInstanceStateReady}, adminSession)
recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/lifecycle/report", dto.RunLifecycleReportRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, ServerInstanceID: server.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: dto.JobProgressBody{Percent: 100, Message: "autonomous start complete"}, Message: "autonomous start complete", ExecutionResult: dto.RunJobExecutionResultBody{Kind: "process", ProcessState: "running", AuditSummary: "private supervised process identity"}})
assertStatus(t, recorder, http.StatusOK)
response := decodeBody[dto.RunLifecycleReportResponse](t, recorder)
if !response.Accepted || response.ProjectedState != domain.ServerInstanceStateRunning {
t.Fatalf("expected lifecycle report projection, got %+v", response)
}
updated := getJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances/"+server.ID, adminSession)
if updated.State != domain.ServerInstanceStateRunning {
t.Fatalf("expected server state projected running, got %+v", updated)
}
}
func TestRunControlAPIReRegistrationRotatesToken(t *testing.T) {
router := newTestRouter()
first := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, validRunControlHelloRequest()))
+30
View File
@@ -120,6 +120,7 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/server-instances/{id}", h.serverInstanceDetail)
mux.HandleFunc("/api/v1/run/control/hello", h.runControlHello)
mux.HandleFunc("/api/v1/run/control/heartbeat", h.requireRunSignature(h.runControlHeartbeat))
mux.HandleFunc("/api/v1/run/lifecycle/report", h.requireRunSignature(h.runLifecycleReport))
mux.HandleFunc("/api/v1/run/jobs/claim", h.requireRunSignature(h.runJobClaim))
mux.HandleFunc("/api/v1/run/jobs/ack", h.requireRunSignature(h.runJobAck))
mux.HandleFunc("/api/v1/run/jobs/progress", h.requireRunSignature(h.runJobProgress))
@@ -1697,6 +1698,35 @@ func (h *coreHandlers) runControlHeartbeat(w http.ResponseWriter, r *http.Reques
writeJSON(w, http.StatusOK, dto.RunControlHeartbeatFromDomain(result))
}
// runLifecycleReport godoc
// @Summary Report autonomous run lifecycle result
// @Description Lets a registered run endpoint report an observed lifecycle terminal result without a platform-assigned job lease.
// @Tags run
// @Accept json
// @Produce json
// @Param body body dto.RunLifecycleReportRequest true "Run lifecycle report request"
// @Success 200 {object} dto.RunLifecycleReportResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/lifecycle/report [post]
func (h *coreHandlers) runLifecycleReport(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunLifecycleReportRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ReportRunLifecycle(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunLifecycleReportFromDomain(result))
}
// runJobClaim godoc
// @Summary Claim one run job
// @Description Lets a registered run endpoint claim one queued job assigned to it using the active session token.
+30
View File
@@ -53,6 +53,26 @@ type RunControlHeartbeatResult struct {
ServerTime time.Time
}
type RunLifecycleReport struct {
RunEndpointID string
SessionToken string
ServerInstanceID string
Capability string
State JobState
Progress RunJobProgressReport
Message string
ErrorCode string
ExecutionResult JobExecutionResult
}
type RunLifecycleReportResult struct {
Accepted bool
RunEndpointID string
ServerInstanceID string
ProjectedState ServerInstanceState
ServerTime time.Time
}
type RunControlSession struct {
RunEndpointID string
SessionToken string `json:"-"`
@@ -103,6 +123,16 @@ func CopyRunControlHeartbeatResult(result RunControlHeartbeatResult) RunControlH
return result
}
func CopyRunLifecycleReport(report RunLifecycleReport) RunLifecycleReport {
report.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(report.ExecutionResult.ServerDeploymentEvidence)
report.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(report.ExecutionResult.DeploymentReceipt)
return report
}
func CopyRunLifecycleReportResult(result RunLifecycleReportResult) RunLifecycleReportResult {
return result
}
func CopyRunControlSession(session RunControlSession) RunControlSession {
session.UsedNonces = CopyStringSlice(session.UsedNonces)
return session
+39
View File
@@ -57,6 +57,26 @@ type RunControlHeartbeatResponse struct {
ServerTime time.Time `json:"serverTime"`
}
type RunLifecycleReportRequest struct {
RunEndpointID string `json:"runEndpointId"`
SessionToken string `json:"sessionToken"`
ServerInstanceID string `json:"serverInstanceId"`
Capability string `json:"capability"`
State domain.JobState `json:"state"`
Progress JobProgressBody `json:"progress"`
Message string `json:"message,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
ExecutionResult RunJobExecutionResultBody `json:"executionResult,omitempty"`
}
type RunLifecycleReportResponse struct {
Accepted bool `json:"accepted"`
RunEndpointID string `json:"runEndpointId"`
ServerInstanceID string `json:"serverInstanceId"`
ProjectedState domain.ServerInstanceState `json:"projectedState,omitempty"`
ServerTime time.Time `json:"serverTime"`
}
func (request RunControlHelloRequest) ToDomain() domain.RunControlHello {
return domain.RunControlHello{
RegistrationToken: request.RegistrationToken,
@@ -92,6 +112,20 @@ func (request RunControlHeartbeatRequest) ToDomain() domain.RunControlHeartbeat
}
}
func (request RunLifecycleReportRequest) ToDomain() domain.RunLifecycleReport {
return domain.RunLifecycleReport{
RunEndpointID: request.RunEndpointID,
SessionToken: request.SessionToken,
ServerInstanceID: request.ServerInstanceID,
Capability: request.Capability,
State: request.State,
Progress: progressReportToDomain(request.Progress),
Message: request.Message,
ErrorCode: request.ErrorCode,
ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)},
}
}
func RunControlHelloFromDomain(result domain.RunControlHelloResult) RunControlHelloResponse {
result = domain.CopyRunControlHelloResult(result)
return RunControlHelloResponse{
@@ -114,3 +148,8 @@ func RunControlHeartbeatFromDomain(result domain.RunControlHeartbeatResult) RunC
ServerTime: result.ServerTime,
}
}
func RunLifecycleReportFromDomain(result domain.RunLifecycleReportResult) RunLifecycleReportResponse {
result = domain.CopyRunLifecycleReportResult(result)
return RunLifecycleReportResponse{Accepted: result.Accepted, RunEndpointID: result.RunEndpointID, ServerInstanceID: result.ServerInstanceID, ProjectedState: result.ProjectedState, ServerTime: result.ServerTime}
}
+2
View File
@@ -63,6 +63,8 @@ Platform-owned Run distribution builds embed an autonomous lifecycle plan for th
The plan is build input for the generated package, not a machine-side job-channel payload. Generated Run registration must not be treated as a trigger to enqueue `process.start`, `process.install`, or `process.status` work; Platform state converges from Run heartbeats, logs, lifecycle reports, supervised process facts, and terminal job/report messages. Platform and Run must not add game-specific hardcoding to interpret the plan.
Autonomous lifecycle reports use `POST /api/v1/run/lifecycle/report` with the active Run session and signed envelope when required. The route accepts only bounded terminal lifecycle facts for `process.install`, `process.start`, `process.stop`, or `process.status`; it validates the server/run binding, records audit evidence, and projects server state from Run-reported process facts without creating or completing a Platform job.
## Log Ingest
Implemented HTTP JSON routes:
+32
View File
@@ -432,6 +432,38 @@ func TestCoreServiceGeneratedSCUMRunRegistrationDoesNotQueueGuidedStart(t *testi
}
}
func TestCoreServiceRunLifecycleReportProjectsGeneratedRunFacts(t *testing.T) {
svc := newTestCoreService()
plugin := createGeneratedRunStatusPlugin(t, svc)
instance := domain.ServerInstance{ID: "managed-autonomous-start", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-autonomous-start"), Name: "Managed Autonomous Start", State: domain.ServerInstanceStateDraft, ConfigVersion: 1, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ProfileKey: "run-local", ServerRoot: `D:\scum-autonomous`, Revision: 1}}
if err := svc.store.ServerInstances().Create(instance); err != nil {
t.Fatalf("create autonomous server: %v", err)
}
registered := registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
reported, err := svc.ReportRunLifecycle(domain.RunLifecycleReport{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, ServerInstanceID: instance.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "autonomous start complete"}, Message: "autonomous start complete", ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "running", AuditSummary: "private supervised process identity"}})
if err != nil || !reported.Accepted || reported.ProjectedState != domain.ServerInstanceStateRunning {
t.Fatalf("expected accepted lifecycle report projected running, result=%+v err=%v", reported, err)
}
stored, err := svc.GetServerInstance(instance.ID)
if err != nil || stored.State != domain.ServerInstanceStateRunning {
t.Fatalf("expected Run report to project server running, server=%+v err=%v", stored, err)
}
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil || len(jobs) != 0 {
t.Fatalf("autonomous lifecycle report must not create platform jobs, jobs=%+v err=%v", jobs, err)
}
other := domain.ServerInstance{ID: "managed-autonomous-other", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-autonomous-other"), Name: "Managed Autonomous Other", State: domain.ServerInstanceStateDraft, ConfigVersion: 1}
if err := svc.store.ServerInstances().Create(other); err != nil {
t.Fatalf("create other server: %v", err)
}
_, err = svc.ReportRunLifecycle(domain.RunLifecycleReport{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, ServerInstanceID: other.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "running"}})
if err == nil || !strings.Contains(err.Error(), "runEndpointId must match server instance") {
t.Fatalf("expected report for another server binding to be rejected, err=%v", err)
}
}
func TestCoreServiceGeneratedRunRegistrationDoesNotDispatchStatusReconciliation(t *testing.T) {
svc := newTestCoreService()
plugin := createGeneratedRunStatusPlugin(t, svc)
+1
View File
@@ -139,6 +139,7 @@ type Core interface {
AckRunJob(domain.RunJobAck) (domain.RunJobAckResult, error)
UpdateRunJobProgress(domain.RunJobProgress) (domain.RunJobProgressResult, error)
CompleteRunJob(domain.RunJobResult) (domain.RunJobResultResult, error)
ReportRunLifecycle(domain.RunLifecycleReport) (domain.RunLifecycleReportResult, error)
GetDistributionBuildInput(domain.DistributionBuildInputRequest) (domain.DistributionBuildInput, error)
GetDependencyExecutionInput(domain.DependencyExecutionInputRequest) (domain.DependencyExecutionInput, error)
DispatchSourceRCONCommandForSession(string, domain.SourceRCONCommandRequest) (domain.SourceRCONCommandDispatch, error)
@@ -2,13 +2,65 @@ package service
import (
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/validator"
)
func (svc *CoreService) ReportRunLifecycle(report domain.RunLifecycleReport) (domain.RunLifecycleReportResult, error) {
report = domain.CopyRunLifecycleReport(report)
if err := validator.ValidateRunLifecycleReport(report); err != nil {
return domain.RunLifecycleReportResult{}, err
}
if _, err := svc.validatedRunSession(report.RunEndpointID, report.SessionToken); err != nil {
return domain.RunLifecycleReportResult{}, err
}
instance, err := svc.store.ServerInstances().Get(report.ServerInstanceID)
if err != nil {
return domain.RunLifecycleReportResult{}, err
}
if instance.RunEndpointID != report.RunEndpointID {
return domain.RunLifecycleReportResult{}, validationError("runEndpointId must match server instance")
}
if instance.State == domain.ServerInstanceStateDeleted {
return domain.RunLifecycleReportResult{}, validationError("server instance must not be deleted")
}
stamp := svc.now()
nextState, projected := lifecycleProjectedState(report.Capability, report.State, report.ExecutionResult)
if projected {
instance.State = nextState
instance.UpdatedAt = stamp
if err := validator.ValidateServerInstance(instance); err != nil {
return domain.RunLifecycleReportResult{}, err
}
if err := svc.store.ServerInstances().Update(instance); err != nil {
return domain.RunLifecycleReportResult{}, err
}
}
auditResult := domain.AuditResultSuccess
if report.State == domain.JobStateFailed || report.State == domain.JobStateCancelled {
auditResult = domain.AuditResultFailed
}
if err := svc.recordAuditEvent("run:"+report.RunEndpointID, "lifecycle.report", "server-instance", instance.ID, auditResult, lifecycleReportSummary(report, nextState, projected)); err != nil {
return domain.RunLifecycleReportResult{}, err
}
return domain.CopyRunLifecycleReportResult(domain.RunLifecycleReportResult{Accepted: true, RunEndpointID: report.RunEndpointID, ServerInstanceID: report.ServerInstanceID, ProjectedState: nextState, ServerTime: stamp}), nil
}
func lifecycleReportSummary(report domain.RunLifecycleReport, projectedState domain.ServerInstanceState, projected bool) string {
for _, candidate := range []string{report.ExecutionResult.AuditSummary, report.Progress.Message, report.Message, report.ErrorCode} {
if strings.TrimSpace(candidate) != "" {
return candidate
}
}
if projected {
return "run reported " + report.Capability + " " + string(report.State) + "; projected server state " + string(projectedState)
}
return "run reported " + report.Capability + " " + string(report.State)
}
func (svc *CoreService) projectRemoteAdapterJobResult(job domain.Job, stamp time.Time) error {
if !strings.HasPrefix(job.Capability, "remote.") || job.ServerInstanceID == "" || !isTerminalJobState(job.State) {
return nil
+34 -1
View File
@@ -54,6 +54,30 @@ func ValidateRunJobResult(result domain.RunJobResult) error {
return finish(violations)
}
func ValidateRunLifecycleReport(report domain.RunLifecycleReport) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", report.RunEndpointID)
violations = appendRequired(violations, "sessionToken", report.SessionToken)
violations = appendRequired(violations, "serverInstanceId", report.ServerInstanceID)
violations = appendRequired(violations, "capability", report.Capability)
if !validLifecycleReportCapability(report.Capability) {
violations = append(violations, "capability must be process.install, process.start, process.stop, or process.status")
}
if !validTerminalJobState(report.State) {
violations = append(violations, "state must be succeeded, failed, or cancelled")
}
violations = appendProgressViolations(violations, report.Progress)
violations = appendMessageLength(violations, "message", report.Message)
violations = appendMessageLength(violations, "errorCode", report.ErrorCode)
if len([]byte(report.ExecutionResult.Content)) > maxJobChannelMessageLength*256 {
violations = append(violations, "executionResult.content is too large")
}
if report.ExecutionResult.Checksum != "" && !validSHA256Checksum(report.ExecutionResult.Checksum) {
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
}
return finish(violations)
}
func ValidateDistributionBuildInputRequest(request domain.DistributionBuildInputRequest) error {
var violations []string
violations = appendLeaseFields(violations, request.RunEndpointID, request.SessionToken, request.JobID, request.LeaseToken, request.Attempt)
@@ -159,7 +183,7 @@ func appendProgressViolations(violations []string, progress domain.RunJobProgres
func validDeploymentProgressPhase(phase string) bool {
switch phase {
case "queued", "claimed", "preflight", "install", "configure", "start", "health":
case "queued", "claimed", "preflight", "install", "configure", "start", "stop", "status", "health":
return true
default:
return false
@@ -181,3 +205,12 @@ func validTerminalJobState(state domain.JobState) bool {
return false
}
}
func validLifecycleReportCapability(capability string) bool {
switch capability {
case domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus:
return true
default:
return false
}
}