Files
browser/platform/service/server_lifecycle_projection.go
T

203 lines
8.0 KiB
Go

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
}
result := domain.AuditResultSuccess
if job.State == domain.JobStateFailed || job.State == domain.JobStateCancelled {
result = domain.AuditResultFailed
}
summary := "remote adapter " + job.Capability + " completed with bounded result reference"
if job.State == domain.JobStateFailed {
summary = "remote adapter " + job.Capability + " failed or timed out; retry/fencing remained platform-owned"
}
if job.State == domain.JobStateCancelled {
summary = "remote adapter " + job.Capability + " was cancelled before terminal projection"
}
return svc.recordAuditEvent("run:"+job.RunEndpointID, "remote-adapter.result", "server-instance", job.ServerInstanceID, result, summary)
}
func (svc *CoreService) projectLifecycleJobResult(job domain.Job, stamp time.Time) error {
if job.Capability == domain.JobCapabilityConfigWrite {
if job.State != domain.JobStateSucceeded {
return svc.recordAuditEvent("run:"+job.RunEndpointID, "config.write.result", "server-instance", job.ServerInstanceID, domain.AuditResultFailed, job.ExecutionResult.AuditSummary)
}
instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID)
if err != nil {
return err
}
instance.ConfigKey = job.TargetKey
instance.ConfigContent = job.ExecutionInput.Content
instance.ConfigChecksum = job.ExecutionResult.Checksum
instance.ConfigVersion = job.ExecutionResult.Version
instance.ConfigUpdatedAt = stamp
instance.UpdatedAt = stamp
if err := validator.ValidateServerInstance(instance); err != nil {
return err
}
if err := svc.store.ServerInstances().Update(instance); err != nil {
return err
}
return svc.recordAuditEvent("run:"+job.RunEndpointID, "config.write.result", "server-instance", instance.ID, domain.AuditResultSuccess, job.ExecutionResult.AuditSummary)
}
nextState, ok := lifecycleProjectedState(job.Capability, job.State, job.ExecutionResult)
if !ok || job.ServerInstanceID == "" {
return nil
}
instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID)
if err != nil {
return err
}
instance.State = nextState
instance.UpdatedAt = stamp
if err := validator.ValidateServerInstance(instance); err != nil {
return err
}
if err := svc.store.ServerInstances().Update(instance); err != nil {
return err
}
auditResult := domain.AuditResultSuccess
if job.State == domain.JobStateFailed || job.State == domain.JobStateCancelled {
auditResult = domain.AuditResultFailed
}
return svc.recordAuditEvent("run:"+job.RunEndpointID, "lifecycle.result", "server-instance", instance.ID, auditResult, job.Progress.Message)
}
func (svc *CoreService) projectServerDeploymentProgress(job domain.Job, stamp time.Time) error {
if job.ExecutionInput.Deployment == nil || job.ServerInstanceID == "" {
return nil
}
instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID)
if err != nil {
return err
}
projection := domain.CopyServerDeploymentProjection(instance.DeploymentProjection)
projection.State = "running"
projection.Operation = job.ExecutionInput.LifecycleOperation
switch job.Progress.Phase {
case "preflight":
projection.PreflightState = "running"
case "scan", "discover", "discovery":
projection.DiscoveryState = "running"
case "install", "configure", "mapping":
projection.MappingState = "running"
case "start", "health":
projection.VerificationState = "running"
}
projection.UpdatedAt = stamp
instance.DeploymentProjection = projection
instance.UpdatedAt = stamp
return svc.store.ServerInstances().Update(instance)
}
func lifecycleProjectedState(capability string, jobState domain.JobState, result domain.JobExecutionResult) (domain.ServerInstanceState, bool) {
if capability != domain.LifecycleCapabilityInstall && capability != domain.LifecycleCapabilityStart && capability != domain.LifecycleCapabilityStop && capability != domain.LifecycleCapabilityStatus {
return "", false
}
if capability == domain.LifecycleCapabilityStatus {
if jobState != domain.JobStateSucceeded {
return "", false
}
return lifecycleStateFromProcessResult(result)
}
if jobState == domain.JobStateFailed || jobState == domain.JobStateCancelled {
return domain.ServerInstanceStateFailed, true
}
if jobState != domain.JobStateSucceeded {
return "", false
}
switch capability {
case domain.LifecycleCapabilityInstall:
return domain.ServerInstanceStateReady, true
case domain.LifecycleCapabilityStart:
return lifecycleStateFromProcessResult(result)
case domain.LifecycleCapabilityStop:
if state, ok := lifecycleStateFromProcessResult(result); ok {
return state, true
}
return domain.ServerInstanceStateStopped, true
default:
return "", false
}
}
func lifecycleStateFromProcessResult(result domain.JobExecutionResult) (domain.ServerInstanceState, bool) {
switch strings.TrimSpace(result.ProcessState) {
case "running":
return domain.ServerInstanceStateRunning, true
case "stopped", "not-started":
return domain.ServerInstanceStateStopped, true
case "exited":
switch strings.TrimSpace(result.ExitClassification) {
case "requested-stop", "forced-stop", "already-stopped":
return domain.ServerInstanceStateStopped, true
default:
return domain.ServerInstanceStateFailed, true
}
default:
return "", false
}
}