Files
browser/platform/service/distribution_build_jobs.go
T

314 lines
11 KiB
Go

package service
import (
"errors"
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBuildInputRequest) (domain.DistributionBuildInput, error) {
if err := validator.ValidateDistributionBuildInputRequest(request); err != nil {
return domain.DistributionBuildInput{}, err
}
session, err := svc.validatedRunSession(request.RunEndpointID, request.SessionToken)
if err != nil {
return domain.DistributionBuildInput{}, err
}
svc.jobMu.Lock()
job, err := svc.fencedJob(session, request.JobID, request.LeaseToken, request.Attempt)
svc.jobMu.Unlock()
if err != nil {
return domain.DistributionBuildInput{}, err
}
if job.Capability != domain.JobCapabilityDistributionBuild {
return domain.DistributionBuildInput{}, validationError("job is not a distribution build")
}
if job.State != domain.JobStateAccepted && job.State != domain.JobStateRunning {
return domain.DistributionBuildInput{}, validationError("distribution build job is not active")
}
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
}
profile, err := svc.clientManagerDistributionBuildProfile(distribution)
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,
PackageFormat: packageFormatForTarget(distribution.TargetOS),
RepositoryURL: distribution.RepositoryURL,
SourceRevision: distribution.SourceRevision,
WorkspaceRef: profile.WorkspaceRef,
EntryRef: profile.EntryRef,
ConfigTemplateRef: clientManagerBuildConfigTemplateRef(profile),
ArtifactID: distribution.ArtifactID,
OutputFilename: clientManagerOutputName(distribution.ProfileKey, distribution.TargetOS),
SecretRef: distribution.SecretRef,
KeyGeneration: distribution.KeyGeneration,
AuthKey: plainKey,
}, nil
}
return domain.DistributionBuildInput{}, repo.ErrNotFound
}
func (svc *CoreService) projectDistributionBuildProgress(job domain.Job, stamp time.Time) error {
if job.Capability != domain.JobCapabilityDistributionBuild {
return nil
}
builds, err := svc.store.ClientManagerBuildJobs().List(domain.ClientManagerBuildJobFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return err
}
for _, build := range builds {
if build.ID != job.ID || build.Status != domain.DistributionJobStatusQueued {
continue
}
build.Status = domain.DistributionJobStatusRunning
build.UpdatedAt = stamp
if err := validator.ValidateClientManagerBuildJob(build); err != nil {
return err
}
return svc.store.ClientManagerBuildJobs().Update(build)
}
return nil
}
func (svc *CoreService) projectDistributionBuildResult(job domain.Job, stamp time.Time) error {
if job.Capability != domain.JobCapabilityDistributionBuild {
return nil
}
if err := svc.validateDistributionBuildResult(job); err != nil {
return err
}
status := domain.DistributionStatusFailed
buildStatus := domain.DistributionJobStatusFailed
var artifact domain.Artifact
if job.State == domain.JobStateSucceeded {
artifactID := strings.TrimPrefix(job.ResultRef, "artifact://")
if artifactID == "" || artifactID == job.ResultRef {
return validationError("distribution build result must reference an artifact")
}
var err error
artifact, err = svc.store.Artifacts().Get(artifactID)
if err != nil {
return err
}
if artifact.State != domain.ArtifactStateAvailable || artifact.OwnerKind != domain.ArtifactOwnerKindJob || artifact.OwnerID != job.ID {
return validationError("distribution build artifact is unavailable or outside the job scope")
}
status = domain.DistributionStatusAvailable
buildStatus = domain.DistributionJobStatusSucceeded
}
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return err
}
for _, distribution := range runDistributions {
if distribution.BuildJobID != job.ID {
continue
}
if distribution.Status == domain.DistributionStatusRevoked {
if artifact.ID != "" {
return svc.expireDistributionArtifact(artifact.ID)
}
return nil
}
if job.State == domain.JobStateSucceeded && artifact.ID != distribution.ArtifactID {
return validationError("distribution build returned an unexpected artifact")
}
distribution.Status = status
if artifact.ID != "" {
distribution.Checksum = artifact.Checksum
}
distribution.UpdatedAt = stamp
if err := validator.ValidateRunDistribution(distribution); err != nil {
return err
}
return svc.store.RunDistributions().Update(distribution)
}
clientDistributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return err
}
for _, distribution := range clientDistributions {
if distribution.BuildJobID != job.ID {
continue
}
if distribution.Status == domain.DistributionStatusRevoked {
if artifact.ID != "" {
return svc.expireDistributionArtifact(artifact.ID)
}
return nil
}
if job.State == domain.JobStateSucceeded && artifact.ID != distribution.ArtifactID {
return validationError("client-manager build returned an unexpected artifact")
}
distribution.Status = status
if artifact.ID != "" {
distribution.Checksum = artifact.Checksum
}
distribution.UpdatedAt = stamp
if err := validator.ValidateClientManagerDistribution(distribution); err != nil {
return err
}
if err := svc.store.ClientManagerDistributions().Update(distribution); err != nil {
return err
}
if err := svc.ProjectClientManagerDistribution(distribution); err != nil {
return err
}
build, err := svc.store.ClientManagerBuildJobs().Get(job.ID)
if err != nil && !errors.Is(err, repo.ErrNotFound) {
return err
}
if err == nil {
build.Status = buildStatus
if artifact.ID != "" {
build.ArtifactID = artifact.ID
build.Checksum = artifact.Checksum
}
build.UpdatedAt = stamp
if err := validator.ValidateClientManagerBuildJob(build); err != nil {
return err
}
if err := svc.store.ClientManagerBuildJobs().Update(build); err != nil {
return err
}
}
return nil
}
return repo.ErrNotFound
}
func (svc *CoreService) validateDistributionBuildResult(job domain.Job) error {
if job.Capability != domain.JobCapabilityDistributionBuild {
return nil
}
expectedArtifactID := ""
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return err
}
for _, distribution := range runDistributions {
if distribution.BuildJobID == job.ID {
expectedArtifactID = distribution.ArtifactID
break
}
}
if expectedArtifactID == "" {
clientDistributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: job.ServerInstanceID})
if err != nil {
return err
}
for _, distribution := range clientDistributions {
if distribution.BuildJobID == job.ID {
expectedArtifactID = distribution.ArtifactID
break
}
}
}
if expectedArtifactID == "" {
return repo.ErrNotFound
}
if job.State != domain.JobStateSucceeded {
return nil
}
artifactID := strings.TrimPrefix(job.ResultRef, "artifact://")
if artifactID == "" || artifactID == job.ResultRef || artifactID != expectedArtifactID {
return validationError("distribution build result must reference the expected artifact")
}
artifact, err := svc.store.Artifacts().Get(artifactID)
if err != nil {
return err
}
if artifact.State != domain.ArtifactStateAvailable || artifact.OwnerKind != domain.ArtifactOwnerKindJob || artifact.OwnerID != job.ID {
return validationError("distribution build artifact is unavailable or outside the job scope")
}
return nil
}
func executableFilename(base string, targetOS string) string {
if targetOS == "windows" {
return base + ".exe"
}
return base
}