feat: 完整游戏运维功能

This commit is contained in:
npc0-hue
2026-07-18 09:04:01 +08:00
parent f3b14b7945
commit 48b8ad8d6c
187 changed files with 16607 additions and 1140 deletions
+615
View File
@@ -0,0 +1,615 @@
package service
import (
"encoding/json"
"errors"
"net/url"
"sort"
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
const runUpdateChunkSize = 1024 * 1024
type dependencyResolution struct {
instance domain.ServerInstance
plugin domain.GamePlugin
binding domain.RuntimeBinding
endpoint domain.RunEndpoint
}
func (svc *CoreService) GetDependencyCatalogForSession(sessionID, serverInstanceID string) (domain.DependencyCatalog, error) {
if _, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID); err != nil {
return domain.DependencyCatalog{}, err
}
resolution, err := svc.resolveDependencyContext(serverInstanceID)
if err != nil {
return domain.DependencyCatalog{}, err
}
statuses, err := svc.store.DependencyStatuses().List(domain.DependencyStatusFilter{ServerInstanceID: serverInstanceID})
if err != nil {
return domain.DependencyCatalog{}, err
}
statusByProbe := map[string]domain.DependencyStatus{}
for _, status := range statuses {
statusByProbe[status.ProbeKey] = status
}
plans := make([]domain.DependencyPlanView, 0, len(resolution.plugin.RuntimeProfiles.InstallPlans))
for _, plan := range resolution.plugin.RuntimeProfiles.InstallPlans {
if !runtimePlatformsContain(plan.Platforms, resolution.endpoint.Platform) {
continue
}
steps := make([]domain.DependencyPlanStepView, len(plan.Steps))
for i, step := range plan.Steps {
host := ""
if parsed, parseErr := url.Parse(step.DownloadRef); parseErr == nil {
host = parsed.Hostname()
}
steps[i] = domain.DependencyPlanStepView{Type: step.Type, TargetKey: step.TargetKey, PackageManager: step.PackageManager, PackageName: step.PackageName, Version: step.Version, DownloadHost: host}
}
var planProbe domain.RuntimeDependencyProbe
for _, candidate := range resolution.plugin.RuntimeProfiles.DependencyProbes {
if runtimePlatformsContain(candidate.Platforms, resolution.endpoint.Platform) && planTargetsProbe(plan, candidate) {
planProbe = candidate
break
}
}
plans = append(plans, domain.DependencyPlanView{Key: plan.Key, Title: plan.Title, TargetOS: resolution.endpoint.Platform, TargetArch: resolution.endpoint.Architecture, Digest: dependencyPlanDigest(resolution, planProbe, plan), Steps: steps})
}
sort.Slice(plans, func(i, j int) bool { return plans[i].Key < plans[j].Key })
probes := make([]domain.DependencyProbeView, 0, len(resolution.plugin.RuntimeProfiles.DependencyProbes))
for _, probe := range resolution.plugin.RuntimeProfiles.DependencyProbes {
if !runtimePlatformsContain(probe.Platforms, resolution.endpoint.Platform) {
continue
}
status := statusByProbe[probe.Key]
planKey := ""
for _, plan := range resolution.plugin.RuntimeProfiles.InstallPlans {
if runtimePlatformsContain(plan.Platforms, resolution.endpoint.Platform) && planTargetsProbe(plan, probe) {
planKey = plan.Key
break
}
}
state := status.State
if state == "" {
state = domain.DependencyStateUnknown
}
probes = append(probes, domain.DependencyProbeView{Key: probe.Key, Kind: probe.Kind, Required: probe.Required, MinimumVersion: probe.MinimumVersion, State: state, Evidence: status.Evidence, InstallPlanKey: planKey})
}
sort.Slice(probes, func(i, j int) bool { return probes[i].Key < probes[j].Key })
return domain.CopyDependencyCatalog(domain.DependencyCatalog{ServerInstanceID: resolution.instance.ID, PluginID: resolution.plugin.ID, PluginVersion: resolution.plugin.Version, ProfileKey: resolution.binding.ProfileKey, TargetOS: resolution.endpoint.Platform, TargetArch: resolution.endpoint.Architecture, Probes: probes, Plans: plans, UpdatedAt: svc.now()}), nil
}
func (svc *CoreService) ListRunUpdateJobsForSession(sessionID, serverInstanceID string) ([]domain.RunUpdateJob, error) {
if _, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID); err != nil {
return nil, err
}
items, err := svc.store.RunUpdateJobs().List(domain.RunUpdateJobFilter{ServerInstanceID: serverInstanceID})
if err != nil {
return nil, err
}
sort.Slice(items, func(i, j int) bool { return items[i].UpdatedAt.After(items[j].UpdatedAt) })
for i := range items {
items[i] = domain.CopyRunUpdateJob(items[i])
}
return items, nil
}
func (svc *CoreService) GetDependencyExecutionInput(request domain.DependencyExecutionInputRequest) (domain.DependencyExecutionInput, error) {
if err := validator.ValidateDependencyExecutionInputRequest(request); err != nil {
return domain.DependencyExecutionInput{}, err
}
job, err := svc.activeFencedInputJob(request.RunEndpointID, request.SessionToken, request.JobID, request.LeaseToken, request.Attempt)
if err != nil {
return domain.DependencyExecutionInput{}, err
}
if job.Capability != domain.JobCapabilityDependenciesCheck && job.Capability != domain.JobCapabilityDependenciesInstall {
return domain.DependencyExecutionInput{}, validationError("job is not a dependency operation")
}
resolution, err := svc.resolveDependencyContext(job.ServerInstanceID)
if err != nil {
return domain.DependencyExecutionInput{}, err
}
if resolution.endpoint.ID != job.RunEndpointID {
return domain.DependencyExecutionInput{}, validationError("dependency endpoint no longer matches")
}
probeKey := strings.TrimPrefix(job.TargetKey, "dependencies/")
if job.Capability == domain.JobCapabilityDependenciesInstall {
probeKey = ""
}
var probe domain.RuntimeDependencyProbe
if probeKey != "" {
probe, err = declaredDependencyProbe(resolution.plugin, probeKey, resolution.endpoint.Platform)
if err != nil {
return domain.DependencyExecutionInput{}, err
}
}
var plan domain.RuntimeInstallPlan
if job.Capability == domain.JobCapabilityDependenciesInstall {
planKey := strings.TrimPrefix(job.TargetKey, "dependencies/install/")
plan, err = declaredInstallPlan(resolution.plugin, planKey, resolution.endpoint.Platform)
if err != nil {
return domain.DependencyExecutionInput{}, err
}
statuses, listErr := svc.store.DependencyStatuses().List(domain.DependencyStatusFilter{ServerInstanceID: job.ServerInstanceID})
if listErr != nil {
return domain.DependencyExecutionInput{}, listErr
}
for _, status := range statuses {
if status.JobID == job.ID {
probe, err = declaredDependencyProbe(resolution.plugin, status.ProbeKey, resolution.endpoint.Platform)
if err != nil {
return domain.DependencyExecutionInput{}, err
}
break
}
}
}
digest := dependencyPlanDigest(resolution, probe, plan)
status, err := svc.dependencyStatusForJob(job.ID, job.ServerInstanceID)
if err != nil {
return domain.DependencyExecutionInput{}, err
}
if status.PlanDigest != digest {
return domain.DependencyExecutionInput{}, validationError("dependency declaration changed after dispatch")
}
bindings, err := dependencyBindings(resolution.binding, probe, plan)
if err != nil {
return domain.DependencyExecutionInput{}, err
}
return domain.CopyDependencyExecutionInput(domain.DependencyExecutionInput{JobID: job.ID, ServerInstanceID: job.ServerInstanceID, RunEndpointID: job.RunEndpointID, PluginID: resolution.plugin.ID, PluginVersion: resolution.plugin.Version, ProfileKey: resolution.binding.ProfileKey, TargetOS: resolution.endpoint.Platform, TargetArch: resolution.endpoint.Architecture, PlanDigest: digest, Probe: probe, Plan: plan, Bindings: bindings}), nil
}
func (svc *CoreService) GetRunUpdateInput(request domain.RunUpdateInputRequest) (domain.RunUpdateInput, error) {
if err := validator.ValidateRunUpdateInputRequest(request); err != nil {
return domain.RunUpdateInput{}, err
}
job, err := svc.activeFencedInputJob(request.RunEndpointID, request.SessionToken, request.JobID, request.LeaseToken, request.Attempt)
if err != nil {
return domain.RunUpdateInput{}, err
}
if job.Capability != domain.JobCapabilityRunSelfUpdate {
return domain.RunUpdateInput{}, validationError("job is not a Run self-update")
}
update, distribution, artifact, err := svc.resolveRunUpdate(job)
if err != nil {
return domain.RunUpdateInput{}, err
}
return domain.RunUpdateInput{JobID: job.ID, ServerInstanceID: job.ServerInstanceID, RunEndpointID: job.RunEndpointID, ArtifactID: artifact.ID, Checksum: artifact.Checksum, SizeBytes: artifact.SizeBytes, TargetOS: distribution.TargetOS, TargetArch: distribution.TargetArch, PackageFormat: distribution.PackageFormat, ExecutableName: executableFilename("run", distribution.TargetOS), TargetRelease: update.TargetRelease, ChunkSizeBytes: runUpdateChunkSize}, nil
}
func (svc *CoreService) ReadRunUpdateChunk(request domain.RunUpdateChunkRequest) (domain.RunUpdateChunk, error) {
if err := validator.ValidateRunUpdateChunkRequest(request); err != nil {
return domain.RunUpdateChunk{}, err
}
job, err := svc.activeFencedInputJob(request.RunEndpointID, request.SessionToken, request.JobID, request.LeaseToken, request.Attempt)
if err != nil {
return domain.RunUpdateChunk{}, err
}
if job.Capability != domain.JobCapabilityRunSelfUpdate {
return domain.RunUpdateChunk{}, validationError("job is not a Run self-update")
}
_, _, artifact, err := svc.resolveRunUpdate(job)
if err != nil {
return domain.RunUpdateChunk{}, err
}
payload, err := svc.artifactPayload(artifact.ID)
if err != nil {
return domain.RunUpdateChunk{}, err
}
if int64(len(payload)) != artifact.SizeBytes || validator.BytesChecksum(payload) != artifact.Checksum {
return domain.RunUpdateChunk{}, validationError("update artifact content does not match metadata")
}
if request.Offset >= artifact.SizeBytes {
return domain.RunUpdateChunk{}, validationError("offset must be inside update artifact")
}
length := request.Length
remaining := artifact.SizeBytes - request.Offset
if int64(length) > remaining {
length = int(remaining)
}
end := request.Offset + int64(length)
return domain.CopyRunUpdateChunk(domain.RunUpdateChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload[int(request.Offset):int(end)], Complete: end == artifact.SizeBytes}), nil
}
func (svc *CoreService) activeFencedInputJob(endpointID, sessionToken, jobID, leaseToken string, attempt int) (domain.Job, error) {
session, err := svc.validatedRunSession(endpointID, sessionToken)
if err != nil {
return domain.Job{}, err
}
svc.jobMu.Lock()
defer svc.jobMu.Unlock()
job, err := svc.fencedJob(session, jobID, leaseToken, attempt)
if err != nil {
return domain.Job{}, err
}
if job.State != domain.JobStateAccepted && job.State != domain.JobStateRunning {
return domain.Job{}, validationError("job input is not active")
}
if !job.CancelRequestedAt.IsZero() {
return domain.Job{}, validationError("job input is cancelled")
}
return job, nil
}
func (svc *CoreService) resolveDependencyContext(serverInstanceID string) (dependencyResolution, error) {
instance, err := svc.store.ServerInstances().Get(serverInstanceID)
if err != nil {
return dependencyResolution{}, err
}
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return dependencyResolution{}, err
}
if plugin.Status != domain.GamePluginStatusInstalled || plugin.Version != instance.PluginVersion {
return dependencyResolution{}, validationError("installed plugin version does not match server")
}
binding, err := svc.runtimeBindingForServer(instance.ID)
if err != nil {
return dependencyResolution{}, err
}
binding, err = normalizeRuntimeBinding(plugin, binding)
if err != nil {
return dependencyResolution{}, err
}
if binding.Status != domain.RuntimeBindingStatusComplete || binding.PluginVersion != plugin.Version {
return dependencyResolution{}, validationError("runtime binding is incomplete or stale")
}
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
return dependencyResolution{}, err
}
if endpoint.Platform == "" || endpoint.Architecture == "" {
return dependencyResolution{}, validationError("Run endpoint target is not registered")
}
return dependencyResolution{instance: instance, plugin: plugin, binding: binding, endpoint: endpoint}, nil
}
func declaredDependencyProbe(plugin domain.GamePlugin, key, targetOS string) (domain.RuntimeDependencyProbe, error) {
for _, probe := range plugin.RuntimeProfiles.DependencyProbes {
if probe.Key == key && runtimePlatformsContain(probe.Platforms, targetOS) {
return probe, nil
}
}
return domain.RuntimeDependencyProbe{}, validationError("dependency probe is not declared for endpoint target")
}
func declaredInstallPlan(plugin domain.GamePlugin, key, targetOS string) (domain.RuntimeInstallPlan, error) {
for _, plan := range plugin.RuntimeProfiles.InstallPlans {
if plan.Key == key && runtimePlatformsContain(plan.Platforms, targetOS) {
return plan, nil
}
}
return domain.RuntimeInstallPlan{}, validationError("dependency install plan is not declared for endpoint target")
}
func runtimePlatformsContain(platforms []string, target string) bool {
if len(platforms) == 0 {
return true
}
for _, platform := range platforms {
if platform == target {
return true
}
}
return false
}
func planTargetsProbe(plan domain.RuntimeInstallPlan, probe domain.RuntimeDependencyProbe) bool {
for _, step := range plan.Steps {
if step.TargetKey == probe.TargetKey {
return true
}
}
return false
}
func dependencyPlanDigest(resolution dependencyResolution, probe domain.RuntimeDependencyProbe, plan domain.RuntimeInstallPlan) string {
keys := make([]string, 0, len(resolution.binding.Bindings))
for key := range resolution.binding.Bindings {
keys = append(keys, key)
}
sort.Strings(keys)
bindingEvidence := make([]string, 0, len(keys))
for _, key := range keys {
bindingEvidence = append(bindingEvidence, key+"="+validator.BytesChecksum([]byte(resolution.binding.Bindings[key])))
}
payload := struct {
PluginID string `json:"pluginId"`
PluginVersion string `json:"pluginVersion"`
ProfileKey string `json:"profileKey"`
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
Binding []string `json:"binding"`
Probe domain.RuntimeDependencyProbe `json:"probe"`
Plan domain.RuntimeInstallPlan `json:"plan"`
}{resolution.plugin.ID, resolution.plugin.Version, resolution.binding.ProfileKey, resolution.endpoint.Platform, resolution.endpoint.Architecture, bindingEvidence, probe, plan}
body, _ := json.Marshal(payload)
return validator.BytesChecksum(body)
}
func dependencyBindings(binding domain.RuntimeBinding, probe domain.RuntimeDependencyProbe, plan domain.RuntimeInstallPlan) (map[string]string, error) {
keys := map[string]struct{}{}
if probe.TargetKey != "" {
keys[probe.TargetKey] = struct{}{}
}
for _, step := range plan.Steps {
if step.TargetKey != "" {
keys[step.TargetKey] = struct{}{}
}
}
out := make(map[string]string, len(keys))
for key := range keys {
value := strings.TrimSpace(binding.Bindings[key])
if value == "" {
value = key
}
lower := strings.ToLower(value)
if strings.HasPrefix(lower, "secret://") || strings.Contains(lower, "password=") || strings.Contains(lower, "token=") {
return nil, validationError("dependency target binding cannot be a secret")
}
out[key] = value
}
return out, nil
}
func (svc *CoreService) dependencyStatusForJob(jobID, serverInstanceID string) (domain.DependencyStatus, error) {
statuses, err := svc.store.DependencyStatuses().List(domain.DependencyStatusFilter{ServerInstanceID: serverInstanceID})
if err != nil {
return domain.DependencyStatus{}, err
}
for _, status := range statuses {
if status.JobID == jobID {
return status, nil
}
}
return domain.DependencyStatus{}, repo.ErrNotFound
}
func (svc *CoreService) resolveRunUpdate(job domain.Job) (domain.RunUpdateJob, domain.RunDistribution, domain.Artifact, error) {
updates, err := svc.store.RunUpdateJobs().List(domain.RunUpdateJobFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, err
}
var update domain.RunUpdateJob
for _, candidate := range updates {
if candidate.JobID == job.ID {
update = candidate
break
}
}
if update.ID == "" || update.RunEndpointID != job.RunEndpointID {
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, validationError("Run update record does not match active job")
}
distributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID, Status: domain.DistributionStatusAvailable})
if err != nil {
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, err
}
var distribution domain.RunDistribution
for _, candidate := range distributions {
if candidate.ArtifactID == update.ArtifactID {
distribution = candidate
break
}
}
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")
}
endpoint, err := svc.store.RunEndpoints().Get(job.RunEndpointID)
if err != nil {
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, err
}
if endpoint.Platform != distribution.TargetOS || endpoint.Architecture != distribution.TargetArch {
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, validationError("Run update target no longer matches endpoint")
}
artifact, err := svc.store.Artifacts().Get(update.ArtifactID)
if err != nil {
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, err
}
if artifact.State != domain.ArtifactStateAvailable || artifact.OwnerKind != domain.ArtifactOwnerKindJob || artifact.OwnerID != distribution.BuildJobID || artifact.Checksum != update.Checksum {
return domain.RunUpdateJob{}, domain.RunDistribution{}, domain.Artifact{}, validationError("Run update artifact is unavailable or outside distribution scope")
}
return update, distribution, artifact, nil
}
func (svc *CoreService) projectDependencyAndRunUpdateResult(job domain.Job, stamp time.Time) error {
if job.Capability == domain.JobCapabilityDependenciesCheck || job.Capability == domain.JobCapabilityDependenciesInstall {
status, err := svc.dependencyStatusForJob(job.ID, job.ServerInstanceID)
if err != nil {
return err
}
status.UpdatedAt = stamp
status.CheckedAt = stamp
status.JobID = job.ID
if job.State == domain.JobStateSucceeded {
var evidence domain.DependencyExecutionEvidence
if err := json.Unmarshal([]byte(job.ExecutionResult.Content), &evidence); err != nil {
return validationError("dependency result evidence is invalid")
}
if evidence.ProbeKey != status.ProbeKey || evidence.PlanDigest != status.PlanDigest || job.ExecutionResult.Checksum != status.PlanDigest {
return validationError("dependency result evidence does not match approved plan")
}
status.State = domain.DependencyState(evidence.State)
status.Evidence = evidence.Evidence
status.CompletedSteps = evidence.CompletedSteps
status.Message = "dependency execution completed"
} else if job.State == domain.JobStateCancelled {
status.State = domain.DependencyStateFailed
status.Message = "dependency execution cancelled"
} else {
status.State = domain.DependencyStateFailed
status.Message = "dependency execution failed"
}
if err := validator.ValidateDependencyStatus(status); err != nil {
return err
}
if err := svc.store.DependencyStatuses().Update(status); err != nil {
return err
}
return svc.recordAuditEvent("run", "dependency.result", "server-instance", job.ServerInstanceID, auditResultForJob(job), status.Message)
}
if job.Capability != domain.JobCapabilityRunSelfUpdate {
return nil
}
update, _, _, err := svc.resolveRunUpdate(job)
if err != nil {
return err
}
update.UpdatedAt = stamp
if job.State == domain.JobStateSucceeded {
var evidence domain.RunUpdateExecutionEvidence
if err := json.Unmarshal([]byte(job.ExecutionResult.Content), &evidence); err != nil || evidence.TargetRelease != update.TargetRelease || evidence.Phase != "staged" || job.ExecutionResult.Checksum != update.Checksum {
return validationError("Run update staged evidence is invalid")
}
update.Status = domain.DistributionJobStatusRunning
update.Phase = domain.RunUpdatePhaseRestartRequested
update.Message = "verified update staged; restart requested"
} else if job.State == domain.JobStateCancelled {
update.Status = domain.DistributionJobStatusFailed
update.Phase = domain.RunUpdatePhaseFailed
update.Message = "Run update cancelled before activation"
} else {
update.Status = domain.DistributionJobStatusFailed
update.Phase = domain.RunUpdatePhaseFailed
update.Message = "Run update verification or staging failed"
}
if err := validator.ValidateRunUpdateJob(update); err != nil {
return err
}
if err := svc.store.RunUpdateJobs().Update(update); err != nil {
return err
}
return svc.recordAuditEvent("run", "run.update.result", "server-instance", job.ServerInstanceID, auditResultForJob(job), update.Message)
}
func (svc *CoreService) projectDependencyAndRunUpdateProgress(job domain.Job, stamp time.Time) error {
if job.Capability == domain.JobCapabilityRunSelfUpdate {
updates, err := svc.store.RunUpdateJobs().List(domain.RunUpdateJobFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return err
}
for _, update := range updates {
if update.JobID != job.ID || update.Status != domain.DistributionJobStatusQueued {
continue
}
update.Status = domain.DistributionJobStatusRunning
update.Phase = domain.RunUpdatePhaseDownloading
update.Message = "Run is downloading and verifying the update"
update.UpdatedAt = stamp
if err := validator.ValidateRunUpdateJob(update); err != nil {
return err
}
return svc.store.RunUpdateJobs().Update(update)
}
}
return nil
}
func (svc *CoreService) ReportRunUpdateHealth(report domain.RunUpdateHealthReport) (domain.RunUpdateHealthResult, error) {
if err := validator.ValidateRunUpdateHealthReport(report); err != nil {
return domain.RunUpdateHealthResult{}, err
}
if _, err := svc.validatedRunSession(report.RunEndpointID, report.SessionToken); err != nil {
return domain.RunUpdateHealthResult{}, err
}
svc.jobMu.Lock()
defer svc.jobMu.Unlock()
job, err := svc.store.Jobs().Get(report.JobID)
if err != nil {
return domain.RunUpdateHealthResult{}, err
}
if job.RunEndpointID != report.RunEndpointID || job.Capability != domain.JobCapabilityRunSelfUpdate || job.State != domain.JobStateSucceeded || job.Attempt != report.Attempt || !leaseTokenMatches(job.LeaseTokenHash, report.LeaseToken) {
return domain.RunUpdateHealthResult{}, validationError("Run update health report does not match terminal attempt")
}
updates, err := svc.store.RunUpdateJobs().List(domain.RunUpdateJobFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return domain.RunUpdateHealthResult{}, err
}
var update domain.RunUpdateJob
for _, candidate := range updates {
if candidate.JobID == job.ID && candidate.RunEndpointID == report.RunEndpointID {
update = candidate
break
}
}
if update.ID == "" || job.ExecutionResult.Checksum != update.Checksum {
return domain.RunUpdateHealthResult{}, validationError("Run update health report does not match staged update")
}
endpoint, err := svc.store.RunEndpoints().Get(report.RunEndpointID)
if err != nil {
return domain.RunUpdateHealthResult{}, err
}
if endpoint.Version != report.Version || endpoint.Status != domain.RunEndpointStatusOnline {
return domain.RunUpdateHealthResult{}, validationError("Run update health version does not match online endpoint")
}
stamp := svc.now()
if report.Outcome == "succeeded" {
if report.Version != update.TargetRelease || update.Phase == domain.RunUpdatePhaseRolledBack || update.Phase == domain.RunUpdatePhaseFailed {
return domain.RunUpdateHealthResult{}, validationError("Run update health version does not match target release")
}
if update.Phase == domain.RunUpdatePhaseSucceeded {
return domain.RunUpdateHealthResult{Accepted: true, JobID: job.ID, Phase: update.Phase, ServerTime: stamp}, nil
}
update.Status = domain.DistributionJobStatusSucceeded
update.Phase = domain.RunUpdatePhaseSucceeded
update.Rollback = false
update.Message = "updated Run registered, reconciled, and reported healthy"
} else {
if update.PreviousVersion != "" && report.Version != update.PreviousVersion {
return domain.RunUpdateHealthResult{}, validationError("rolled-back Run version does not match previous release")
}
if update.Phase == domain.RunUpdatePhaseRolledBack {
return domain.RunUpdateHealthResult{Accepted: true, JobID: job.ID, Phase: update.Phase, ServerTime: stamp}, nil
}
update.Status = domain.DistributionJobStatusFailed
update.Phase = domain.RunUpdatePhaseRolledBack
update.Rollback = true
update.Message = "Run update activation failed and previous executable was restored"
}
update.UpdatedAt = stamp
if err := validator.ValidateRunUpdateJob(update); err != nil {
return domain.RunUpdateHealthResult{}, err
}
if err := svc.store.RunUpdateJobs().Update(update); err != nil {
return domain.RunUpdateHealthResult{}, err
}
auditResult := domain.AuditResultSuccess
if report.Outcome == "rolled-back" {
auditResult = domain.AuditResultFailed
}
if err := svc.recordAuditEvent("run", "run.update.health", "server-instance", update.ServerInstanceID, auditResult, update.Message); err != nil {
return domain.RunUpdateHealthResult{}, err
}
return domain.RunUpdateHealthResult{Accepted: true, JobID: job.ID, Phase: update.Phase, ServerTime: stamp}, nil
}
func auditResultForJob(job domain.Job) domain.AuditResult {
if job.State == domain.JobStateSucceeded {
return domain.AuditResultSuccess
}
if job.State == domain.JobStateCancelled {
return domain.AuditResultDenied
}
return domain.AuditResultFailed
}
func sameRunUpdateTarget(existing, expected domain.RunUpdateJob) bool {
return existing.ServerInstanceID == expected.ServerInstanceID && existing.RunEndpointID == expected.RunEndpointID && existing.ArtifactID == expected.ArtifactID && existing.Checksum == expected.Checksum && existing.TargetOS == expected.TargetOS && existing.TargetArch == expected.TargetArch && existing.TargetRelease == expected.TargetRelease && existing.JobID == expected.JobID && existing.IdempotencyKey == expected.IdempotencyKey
}
func findRunDistributionForArtifact(distributions []domain.RunDistribution, artifactID string) (domain.RunDistribution, error) {
for _, distribution := range distributions {
if distribution.ArtifactID == artifactID && distribution.Status == domain.DistributionStatusAvailable {
return distribution, nil
}
}
return domain.RunDistribution{}, errors.New("available Run distribution not found")
}