542 lines
21 KiB
Go
542 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/repo"
|
|
"browser.local/platform/validator"
|
|
)
|
|
|
|
const platformDistributionBuilderEndpointID = "platform-distribution-builder"
|
|
|
|
// unconfiguredDistributionBuilder stands in when no platform builder has been
|
|
// installed. It reports a platform-builder reason so an operator is not sent
|
|
// looking at run endpoint capabilities.
|
|
type unconfiguredDistributionBuilder struct{}
|
|
|
|
func (unconfiguredDistributionBuilder) Readiness() (bool, string) {
|
|
return false, "platform builder is not configured"
|
|
}
|
|
|
|
func (unconfiguredDistributionBuilder) Build(domain.DistributionBuildInput) ([]byte, error) {
|
|
return nil, validationError("platform builder is not configured")
|
|
}
|
|
|
|
func (svc *CoreService) configuredDistributionBuilder() DistributionBuilder {
|
|
svc.distributionBuildMu.Lock()
|
|
defer svc.distributionBuildMu.Unlock()
|
|
if svc.distributionBuilder == nil {
|
|
return unconfiguredDistributionBuilder{}
|
|
}
|
|
return svc.distributionBuilder
|
|
}
|
|
|
|
// distributionBuilderReadiness reports platform builder readiness. The reason
|
|
// always names the platform builder, never a run endpoint capability.
|
|
func (svc *CoreService) distributionBuilderReadiness() (bool, string) {
|
|
builder := svc.configuredDistributionBuilder()
|
|
ready, reason := builder.Readiness()
|
|
if ready {
|
|
return true, ""
|
|
}
|
|
reason = strings.TrimSpace(reason)
|
|
if reason == "" {
|
|
reason = "platform builder is unavailable"
|
|
} else if !strings.Contains(strings.ToLower(reason), "platform builder") {
|
|
reason = "platform builder is unavailable: " + reason
|
|
}
|
|
return false, reason
|
|
}
|
|
|
|
func (svc *CoreService) enqueueDistributionBuild(job domain.Job) {
|
|
svc.distributionBuildMu.Lock()
|
|
if _, exists := svc.distributionBuilds[job.ID]; exists {
|
|
svc.distributionBuildMu.Unlock()
|
|
return
|
|
}
|
|
svc.distributionBuilds[job.ID] = struct{}{}
|
|
svc.distributionBuildMu.Unlock()
|
|
go func() {
|
|
defer func() {
|
|
svc.distributionBuildMu.Lock()
|
|
delete(svc.distributionBuilds, job.ID)
|
|
svc.distributionBuildMu.Unlock()
|
|
}()
|
|
_ = svc.executeDistributionBuild(job)
|
|
}()
|
|
}
|
|
|
|
// executeDistributionBuild claims the build job internally and completes it with
|
|
// the artifact produced by the platform builder. Build work is never dispatched
|
|
// to a machine-side run endpoint, so the component auth key stays on the
|
|
// platform.
|
|
func (svc *CoreService) executeDistributionBuild(job domain.Job) error {
|
|
if job.Capability != domain.JobCapabilityDistributionBuild {
|
|
return validationError("job is not a distribution build")
|
|
}
|
|
input, err := svc.platformDistributionBuildInput(job)
|
|
if err != nil {
|
|
return svc.failDistributionBuildJob(job, "platform builder could not assemble build input")
|
|
}
|
|
if err := svc.markDistributionBuildRunning(&job); err != nil {
|
|
return err
|
|
}
|
|
if isTerminalJobState(job.State) {
|
|
return nil
|
|
}
|
|
builder := svc.configuredDistributionBuilder()
|
|
var payload []byte
|
|
var buildErr error
|
|
if progressBuilder, ok := builder.(distributionBuilderWithProgress); ok {
|
|
payload, buildErr = progressBuilder.BuildWithProgress(input, func(progress DistributionBuildProgress) {
|
|
_ = svc.updateDistributionBuildProgress(job, progress)
|
|
})
|
|
} else {
|
|
payload, buildErr = builder.Build(input)
|
|
}
|
|
if buildErr != nil {
|
|
return svc.failDistributionBuildJob(job, builderJobFailureMessage(buildErr))
|
|
}
|
|
if _, err := svc.platformDistributionBuildInput(job); err != nil {
|
|
return svc.failDistributionBuildJob(job, "platform builder discarded output because the component key is no longer current")
|
|
}
|
|
if err := svc.storeDistributionBuildArtifact(input.ArtifactID, job.ID, payload); err != nil {
|
|
return svc.failDistributionBuildJob(job, "platform builder could not record the distribution artifact")
|
|
}
|
|
return svc.succeedDistributionBuildJob(job, input.ArtifactID)
|
|
}
|
|
|
|
// platformDistributionBuildInput resolves the build input, including the
|
|
// component auth key, inside the platform. Unlike GetDistributionBuildInput it
|
|
// never crosses the job channel.
|
|
func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.DistributionBuildInput, error) {
|
|
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
|
if err != nil {
|
|
return domain.DistributionBuildInput{}, err
|
|
}
|
|
for _, distribution := range runDistributions {
|
|
if distribution.BuildJobID != job.ID {
|
|
continue
|
|
}
|
|
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentRun, "")
|
|
if err != nil {
|
|
return domain.DistributionBuildInput{}, err
|
|
}
|
|
if key.Generation != distribution.KeyGeneration {
|
|
return domain.DistributionBuildInput{}, validationError("run build key generation is no longer current")
|
|
}
|
|
plainKey, err := svc.decryptRuntimeKey(key.EncryptedKey)
|
|
if err != nil {
|
|
return domain.DistributionBuildInput{}, err
|
|
}
|
|
packageInput, err := svc.runDistributionPackageContext(distribution)
|
|
if err != nil {
|
|
return domain.DistributionBuildInput{}, err
|
|
}
|
|
return domain.DistributionBuildInput{
|
|
JobID: job.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
ServerInstanceID: distribution.ServerInstanceID,
|
|
PluginID: distribution.PluginID,
|
|
RunEndpointID: distribution.RunEndpointID,
|
|
ProfileKey: packageInput.profileKey,
|
|
TargetOS: distribution.TargetOS,
|
|
TargetArch: distribution.TargetArch,
|
|
TargetRelease: distribution.ID,
|
|
PlatformURL: runReleasePlatformURL(),
|
|
PackageFormat: distribution.PackageFormat,
|
|
ArtifactID: distribution.ArtifactID,
|
|
OutputFilename: executableFilename("run", distribution.TargetOS),
|
|
SecretRef: distribution.SecretRef,
|
|
KeyGeneration: distribution.KeyGeneration,
|
|
AuthKey: plainKey,
|
|
WorkspaceSeed: packageInput.workspaceSeed,
|
|
AutonomousLifecycle: packageInput.autonomousLifecycle,
|
|
}, nil
|
|
}
|
|
|
|
clientDistributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
|
if err != nil {
|
|
return domain.DistributionBuildInput{}, err
|
|
}
|
|
for _, distribution := range clientDistributions {
|
|
if distribution.BuildJobID != job.ID {
|
|
continue
|
|
}
|
|
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentClientManager, distribution.ProfileKey)
|
|
if err != nil {
|
|
return domain.DistributionBuildInput{}, err
|
|
}
|
|
if key.Generation != distribution.KeyGeneration {
|
|
return domain.DistributionBuildInput{}, validationError("client-manager build key generation is no longer current")
|
|
}
|
|
plainKey, err := svc.decryptRuntimeKey(key.EncryptedKey)
|
|
if err != nil {
|
|
return domain.DistributionBuildInput{}, err
|
|
}
|
|
return domain.DistributionBuildInput{
|
|
JobID: job.ID,
|
|
ComponentKind: domain.DistributionComponentClientManager,
|
|
ServerInstanceID: distribution.ServerInstanceID,
|
|
PluginID: distribution.PluginID,
|
|
RunEndpointID: job.RunEndpointID,
|
|
ProfileKey: distribution.ProfileKey,
|
|
TargetOS: distribution.TargetOS,
|
|
TargetArch: distribution.TargetArch,
|
|
PlatformURL: runReleasePlatformURL(),
|
|
PackageFormat: packageFormatForTarget(distribution.TargetOS),
|
|
RepositoryURL: distribution.RepositoryURL,
|
|
SourceRevision: distribution.SourceRevision,
|
|
ArtifactID: distribution.ArtifactID,
|
|
OutputFilename: clientManagerOutputName(distribution.ProfileKey, distribution.TargetOS),
|
|
SecretRef: distribution.SecretRef,
|
|
KeyGeneration: distribution.KeyGeneration,
|
|
AuthKey: plainKey,
|
|
}, nil
|
|
}
|
|
return domain.DistributionBuildInput{}, repo.ErrNotFound
|
|
}
|
|
|
|
type runDistributionPackageContext struct {
|
|
profileKey string
|
|
workspaceSeed string
|
|
autonomousLifecycle *domain.RunAutonomousLifecyclePlan
|
|
}
|
|
|
|
func (svc *CoreService) runDistributionPackageContext(distribution domain.RunDistribution) (runDistributionPackageContext, error) {
|
|
instance, err := svc.store.ServerInstances().Get(distribution.ServerInstanceID)
|
|
if err != nil {
|
|
return runDistributionPackageContext{}, err
|
|
}
|
|
plugin, err := svc.store.GamePlugins().Get(distribution.PluginID)
|
|
if err != nil {
|
|
return runDistributionPackageContext{}, err
|
|
}
|
|
profileKey := instance.Deployment.ProfileKey
|
|
bindings := map[string]string(nil)
|
|
if binding, bindingErr := svc.runtimeBindingForServer(distribution.ServerInstanceID); bindingErr == nil {
|
|
profileKey = binding.ProfileKey
|
|
bindings = domain.CopyStringMap(binding.Bindings)
|
|
} else if !errors.Is(bindingErr, repo.ErrNotFound) {
|
|
return runDistributionPackageContext{}, bindingErr
|
|
}
|
|
plan, err := runAutonomousLifecyclePlan(distribution, instance, plugin, profileKey, bindings)
|
|
if err != nil {
|
|
return runDistributionPackageContext{}, err
|
|
}
|
|
seed, err := encodeRunWorkspaceSeed(plugin.LifecycleAssets, plan)
|
|
if err != nil {
|
|
return runDistributionPackageContext{}, err
|
|
}
|
|
return runDistributionPackageContext{profileKey: profileKey, workspaceSeed: seed, autonomousLifecycle: plan}, nil
|
|
}
|
|
|
|
func runAutonomousLifecyclePlan(distribution domain.RunDistribution, instance domain.ServerInstance, plugin domain.GamePlugin, profileKey string, bindings map[string]string) (*domain.RunAutonomousLifecyclePlan, error) {
|
|
plan := &domain.RunAutonomousLifecyclePlan{
|
|
SchemaVersion: "1",
|
|
ServerInstanceID: instance.ID,
|
|
PluginID: plugin.ID,
|
|
PluginVersion: plugin.Version,
|
|
RunEndpointID: distribution.RunEndpointID,
|
|
ProfileKey: profileKey,
|
|
TargetOS: distribution.TargetOS,
|
|
TargetArch: distribution.TargetArch,
|
|
TargetRelease: distribution.ID,
|
|
DeploymentRevision: instance.Deployment.Revision,
|
|
RuntimeBindings: domain.CopyStringMap(bindings),
|
|
Deployment: autonomousDeploymentFromDefinition(instance.Deployment, profileKey, bindings),
|
|
}
|
|
profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, profileKey)
|
|
for _, action := range []domain.ServerLifecycleAction{domain.ServerLifecycleActionCreate, domain.ServerLifecycleActionStart, domain.ServerLifecycleActionStop, domain.ServerLifecycleActionStatus} {
|
|
entry := autonomousLifecycleAction(plugin, profile, hasProfile, action)
|
|
if entry.TargetKey == "" {
|
|
continue
|
|
}
|
|
plan.Actions = append(plan.Actions, entry)
|
|
}
|
|
bootstrap := autonomousLifecycleAction(plugin, profile, hasProfile, autonomousBootstrapLifecycleAction(plugin, profile, hasProfile))
|
|
if bootstrap.TargetKey != "" {
|
|
plan.Bootstrap = &bootstrap
|
|
}
|
|
for _, probe := range plugin.RuntimeProfiles.DependencyProbes {
|
|
if runtimePlatformsContain(probe.Platforms, distribution.TargetOS) {
|
|
plan.DependencyProbes = append(plan.DependencyProbes, autonomousDependencyProbe(probe))
|
|
}
|
|
}
|
|
for _, installPlan := range plugin.RuntimeProfiles.InstallPlans {
|
|
if runtimePlatformsContain(installPlan.Platforms, distribution.TargetOS) {
|
|
plan.InstallPlans = append(plan.InstallPlans, autonomousInstallPlan(installPlan))
|
|
}
|
|
}
|
|
for _, source := range plugin.RuntimeProfiles.LogSources {
|
|
if autonomousRunLogSourceKind(source.Kind) {
|
|
plan.LogSources = append(plan.LogSources, autonomousLogSource(source))
|
|
}
|
|
}
|
|
if hasProfile && len(profile.DLLExtensionRefs) > 0 {
|
|
endpoint := domain.RunEndpoint{ID: distribution.RunEndpointID, Platform: distribution.TargetOS, Architecture: distribution.TargetArch}
|
|
extensions, err := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, extension := range extensions {
|
|
plan.DLLExtensions = append(plan.DLLExtensions, autonomousDLLExtension(extension))
|
|
}
|
|
}
|
|
return domain.CopyRunAutonomousLifecyclePlanPtr(plan), nil
|
|
}
|
|
|
|
func autonomousLifecycleAction(plugin domain.GamePlugin, profile domain.RuntimeLifecycleProfile, hasProfile bool, action domain.ServerLifecycleAction) domain.RunAutonomousLifecycleAction {
|
|
targetKey := ""
|
|
if hasProfile {
|
|
targetKey = runtimeProfileActionRef(profile.ActionRefs, action)
|
|
}
|
|
if targetKey == "" {
|
|
targetKey = lifecycleActionRef(plugin, action)
|
|
}
|
|
if targetKey == "" {
|
|
return domain.RunAutonomousLifecycleAction{}
|
|
}
|
|
return domain.RunAutonomousLifecycleAction{Action: action, Operation: lifecycleExecutionOperation(action), Capability: domain.LifecycleCapabilityForAction(action), TargetKey: targetKey}
|
|
}
|
|
|
|
func autonomousBootstrapLifecycleAction(plugin domain.GamePlugin, profile domain.RuntimeLifecycleProfile, hasProfile bool) domain.ServerLifecycleAction {
|
|
if autonomousLifecycleAction(plugin, profile, hasProfile, domain.ServerLifecycleActionStart).TargetKey != "" {
|
|
return domain.ServerLifecycleActionStart
|
|
}
|
|
return domain.ServerLifecycleActionCreate
|
|
}
|
|
|
|
func autonomousDependencyProbe(probe domain.RuntimeDependencyProbe) domain.RunAutonomousDependencyProbe {
|
|
return domain.RunAutonomousDependencyProbe{Key: probe.Key, Kind: probe.Kind, TargetKey: probe.TargetKey, Required: probe.Required, MinimumVersion: probe.MinimumVersion, Platforms: domain.CopyStringSlice(probe.Platforms)}
|
|
}
|
|
|
|
func autonomousInstallPlan(plan domain.RuntimeInstallPlan) domain.RunAutonomousInstallPlan {
|
|
steps := make([]domain.RunAutonomousInstallStep, len(plan.Steps))
|
|
for i, step := range plan.Steps {
|
|
steps[i] = domain.RunAutonomousInstallStep{Type: step.Type, TargetKey: step.TargetKey, PackageManager: step.PackageManager, PackageName: step.PackageName, Version: step.Version, DownloadRef: step.DownloadRef, Checksum: step.Checksum}
|
|
}
|
|
return domain.RunAutonomousInstallPlan{Key: plan.Key, Title: plan.Title, Platforms: domain.CopyStringSlice(plan.Platforms), Steps: steps}
|
|
}
|
|
|
|
func autonomousLogSource(source domain.RuntimeLogSource) domain.RunAutonomousLogSource {
|
|
return domain.RunAutonomousLogSource{Key: source.Key, Kind: source.Kind, TargetKey: source.TargetKey, StreamKey: source.StreamKey, CursorKind: source.CursorKind, RetentionDays: source.RetentionDays}
|
|
}
|
|
|
|
func autonomousRunLogSourceKind(kind string) bool {
|
|
return kind == "process.stdout" || kind == "process.stderr" || kind == "file.tail"
|
|
}
|
|
|
|
func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.RunAutonomousDLLExtension {
|
|
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
|
}
|
|
|
|
func autonomousDeploymentFromDefinition(definition domain.ServerDeploymentDefinition, profileKey string, bindings map[string]string) *domain.RunAutonomousDeployment {
|
|
if definition.Mode == "" {
|
|
return nil
|
|
}
|
|
copy := domain.CopyServerDeploymentDefinition(definition)
|
|
if copy.ProfileKey == "" {
|
|
copy.ProfileKey = profileKey
|
|
}
|
|
if len(copy.RuntimeBindings) == 0 {
|
|
copy.RuntimeBindings = domain.CopyStringMap(bindings)
|
|
}
|
|
return &domain.RunAutonomousDeployment{SchemaVersion: "1", Mode: copy.Mode, ProfileKey: copy.ProfileKey, RuntimeBindings: copy.RuntimeBindings, CreateInputs: copy.CreateInputs, ServerRoot: copy.ServerRoot, WorkingDirectory: copy.WorkingDirectory, InstallCommand: copy.InstallCommand, StartCommand: copy.StartCommand, StopCommand: copy.StopCommand, StatusCommand: copy.StatusCommand, Shell: copy.Shell, Revision: copy.Revision}
|
|
}
|
|
|
|
func encodeRunWorkspaceSeed(files []domain.PluginAssetFile, plan *domain.RunAutonomousLifecyclePlan) (string, error) {
|
|
seedFiles := append([]domain.PluginAssetFile(nil), files...)
|
|
if plan != nil {
|
|
payload, err := json.Marshal(plan)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
seedFiles = append(seedFiles, domain.PluginAssetFile{Path: ".platform/autonomous-lifecycle-plan.json", Content: string(payload), Mode: 0o600})
|
|
}
|
|
return encodePluginWorkspaceSeed(seedFiles)
|
|
}
|
|
|
|
func encodePluginWorkspaceSeed(files []domain.PluginAssetFile) (string, error) {
|
|
if len(files) == 0 {
|
|
return "", nil
|
|
}
|
|
type seedFile struct {
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
Mode int `json:"mode,omitempty"`
|
|
}
|
|
seed := make([]seedFile, len(files))
|
|
for i, file := range files {
|
|
seed[i] = seedFile{Path: file.Path, Content: file.Content, Mode: file.Mode}
|
|
}
|
|
body, err := json.Marshal(seed)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(body), nil
|
|
}
|
|
|
|
// storeDistributionBuildArtifact records the built package under job ownership
|
|
// so the existing ArtifactOwnerKindJob scope assertions keep guarding it.
|
|
func (svc *CoreService) storeDistributionBuildArtifact(artifactID string, jobID string, payload []byte) error {
|
|
if strings.TrimSpace(artifactID) == "" {
|
|
return validationError("distribution build artifact id is required")
|
|
}
|
|
if len(payload) == 0 {
|
|
return validationError("distribution build produced no package bytes")
|
|
}
|
|
stamp := svc.now()
|
|
svc.artifactMu.Lock()
|
|
defer svc.artifactMu.Unlock()
|
|
|
|
artifact, err := svc.store.Artifacts().Get(artifactID)
|
|
if err != nil && !errors.Is(err, repo.ErrNotFound) {
|
|
return err
|
|
}
|
|
create := errors.Is(err, repo.ErrNotFound)
|
|
if create {
|
|
artifact = domain.Artifact{ID: artifactID, OwnerKind: domain.ArtifactOwnerKindJob, OwnerID: jobID, CreatedAt: stamp}
|
|
}
|
|
if artifact.OwnerKind != domain.ArtifactOwnerKindJob || artifact.OwnerID != jobID {
|
|
return validationError("distribution build artifact is outside the job scope")
|
|
}
|
|
artifact.SizeBytes = int64(len(payload))
|
|
artifact.Checksum = validator.BytesChecksum(payload)
|
|
artifact.State = domain.ArtifactStateAvailable
|
|
artifact.UpdatedAt = stamp
|
|
if err := validator.ValidateArtifact(artifact); err != nil {
|
|
return err
|
|
}
|
|
if err := svc.artifactStore.PutPayload(artifact.ID, payload); err != nil {
|
|
return err
|
|
}
|
|
if create {
|
|
if err := svc.store.Artifacts().Create(artifact); err != nil {
|
|
return err
|
|
}
|
|
} else if err := svc.store.Artifacts().Update(artifact); err != nil {
|
|
return err
|
|
}
|
|
svc.artifactPayloads[artifact.ID] = domain.CopyBytes(payload)
|
|
return nil
|
|
}
|
|
|
|
func (svc *CoreService) markDistributionBuildRunning(job *domain.Job) error {
|
|
stamp := svc.now()
|
|
svc.jobMu.Lock()
|
|
defer svc.jobMu.Unlock()
|
|
|
|
current, err := svc.store.Jobs().Get(job.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isTerminalJobState(current.State) {
|
|
*job = current
|
|
return nil
|
|
}
|
|
current.State = domain.JobStateRunning
|
|
current.Attempt = maxInt(current.Attempt, 1)
|
|
current.Progress = domain.JobProgress{Percent: 5, Phase: current.Progress.Phase, Message: "env_check: platform builder started"}
|
|
current.UpdatedAt = stamp
|
|
if err := svc.updateScheduledJob(current); err != nil {
|
|
return err
|
|
}
|
|
if err := svc.projectDistributionBuildProgress(current, stamp); err != nil {
|
|
return err
|
|
}
|
|
*job = current
|
|
return nil
|
|
}
|
|
|
|
func (svc *CoreService) updateDistributionBuildProgress(job domain.Job, progress DistributionBuildProgress) error {
|
|
stamp := svc.now()
|
|
svc.jobMu.Lock()
|
|
defer svc.jobMu.Unlock()
|
|
|
|
current, err := svc.store.Jobs().Get(job.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isTerminalJobState(current.State) {
|
|
return nil
|
|
}
|
|
if progress.Percent < current.Progress.Percent {
|
|
progress.Percent = current.Progress.Percent
|
|
}
|
|
current.Progress = domain.JobProgress{Percent: progress.Percent, Phase: current.Progress.Phase, Message: strings.TrimSpace(progress.Message)}
|
|
current.UpdatedAt = stamp
|
|
if err := svc.updateScheduledJob(current); err != nil {
|
|
return err
|
|
}
|
|
return svc.projectDistributionBuildProgress(current, stamp)
|
|
}
|
|
|
|
func (svc *CoreService) succeedDistributionBuildJob(job domain.Job, artifactID string) error {
|
|
stamp := svc.now()
|
|
svc.jobMu.Lock()
|
|
current, err := svc.store.Jobs().Get(job.ID)
|
|
if err != nil {
|
|
svc.jobMu.Unlock()
|
|
return err
|
|
}
|
|
if isTerminalJobState(current.State) {
|
|
if current.State == domain.JobStateSucceeded && current.ResultRef == "artifact://"+artifactID {
|
|
svc.jobMu.Unlock()
|
|
return nil
|
|
}
|
|
svc.jobMu.Unlock()
|
|
_ = svc.expireDistributionArtifact(artifactID)
|
|
return nil
|
|
}
|
|
current.State = domain.JobStateSucceeded
|
|
current.ResultRef = "artifact://" + artifactID
|
|
current.Progress = domain.JobProgress{Percent: 100, Phase: current.Progress.Phase, Message: "platform builder completed"}
|
|
current.LeaseTokenHash = ""
|
|
current.TerminalAt = stamp
|
|
current.TerminalFingerprint = "platform-builder:succeeded:" + artifactID
|
|
current.UpdatedAt = stamp
|
|
if err := svc.updateScheduledJob(current); err != nil {
|
|
svc.jobMu.Unlock()
|
|
return err
|
|
}
|
|
svc.jobMu.Unlock()
|
|
return svc.projectDistributionBuildResult(current, stamp)
|
|
}
|
|
|
|
func (svc *CoreService) failDistributionBuildJob(job domain.Job, reason string) error {
|
|
stamp := svc.now()
|
|
if strings.TrimSpace(reason) == "" {
|
|
reason = "platform builder failed"
|
|
}
|
|
svc.jobMu.Lock()
|
|
current, err := svc.store.Jobs().Get(job.ID)
|
|
if err != nil {
|
|
svc.jobMu.Unlock()
|
|
return err
|
|
}
|
|
if isTerminalJobState(current.State) {
|
|
svc.jobMu.Unlock()
|
|
return nil
|
|
}
|
|
current.State = domain.JobStateFailed
|
|
current.Progress = domain.JobProgress{Percent: current.Progress.Percent, Phase: current.Progress.Phase, Message: reason}
|
|
current.LeaseTokenHash = ""
|
|
current.TerminalAt = stamp
|
|
current.TerminalFingerprint = "platform-builder:failed:" + reason
|
|
current.UpdatedAt = stamp
|
|
if err := svc.updateScheduledJob(current); err != nil {
|
|
svc.jobMu.Unlock()
|
|
return err
|
|
}
|
|
svc.jobMu.Unlock()
|
|
if err := svc.projectDistributionBuildResult(current, stamp); err != nil && !errors.Is(err, repo.ErrNotFound) {
|
|
return err
|
|
}
|
|
return validationError(reason)
|
|
}
|