feat: 自动更新
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -78,6 +77,13 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "run.generate.denied"); err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
if err := validateRunnableEndpoint(endpoint, domain.JobCapabilityDistributionBuild); err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
|
||||
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
||||
if err != nil {
|
||||
@@ -90,26 +96,9 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
|
||||
config := generatedPackageConfig{
|
||||
Kind: string(domain.DistributionComponentRun),
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
SecretRef: key.SecretRef,
|
||||
KeyGeneration: key.Generation,
|
||||
AuthKey: plainKey,
|
||||
}
|
||||
payload, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
artifactID := artifactIDForDistribution(distributionID)
|
||||
artifact, err := svc.createPlatformArtifactPayload(artifactID, domain.ArtifactOwnerKindServerInstance, instance.ID, payload)
|
||||
if err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
_ = plainKey
|
||||
artifactID := artifactIDForDistribution(distributionID + "-binary")
|
||||
buildJobID := jobIDFromParts("job-distribution-build", instance.ID, distributionID)
|
||||
stamp := svc.now()
|
||||
distribution := domain.RunDistribution{
|
||||
ID: distributionID,
|
||||
@@ -119,11 +108,11 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
PackageFormat: packageFormatForTarget(request.TargetOS),
|
||||
ArtifactID: artifact.ID,
|
||||
Checksum: artifact.Checksum,
|
||||
BuildJobID: buildJobID,
|
||||
ArtifactID: artifactID,
|
||||
KeyGeneration: key.Generation,
|
||||
SecretRef: key.SecretRef,
|
||||
Status: domain.DistributionStatusAvailable,
|
||||
Status: domain.DistributionStatusBuilding,
|
||||
CreatedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
}
|
||||
@@ -131,9 +120,35 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
if err := svc.store.RunDistributions().Create(distribution); err != nil {
|
||||
if errors.Is(err, repo.ErrDuplicate) {
|
||||
existing, getErr := svc.store.RunDistributions().Get(distribution.ID)
|
||||
if getErr != nil {
|
||||
return domain.RunDistribution{}, getErr
|
||||
}
|
||||
return domain.CopyRunDistribution(existing), nil
|
||||
}
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
if err := svc.recordAuditEvent(user.ID, "run.generate", "server-instance", instance.ID, domain.AuditResultSuccess, "generated run package with redacted runtime key ref"); err != nil {
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: buildJobID,
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: domain.JobCapabilityDistributionBuild,
|
||||
TargetKey: "distribution/run",
|
||||
InputRef: "input://distribution-build/" + distribution.ID,
|
||||
IdempotencyKey: "distribution-build:" + distribution.ID,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "build queued"},
|
||||
})
|
||||
if err != nil {
|
||||
distribution.Status = domain.DistributionStatusFailed
|
||||
distribution.UpdatedAt = svc.now()
|
||||
_ = svc.store.RunDistributions().Update(distribution)
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
if job.ID != buildJobID || job.Capability != domain.JobCapabilityDistributionBuild {
|
||||
return domain.RunDistribution{}, validationError("distribution build idempotency key conflicts with another job")
|
||||
}
|
||||
if err := svc.recordAuditEvent(user.ID, "run.generate", "server-instance", instance.ID, domain.AuditResultQueued, "queued run binary build job with redacted runtime key ref"); err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
return domain.CopyRunDistribution(distribution), nil
|
||||
@@ -172,6 +187,13 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "client-manager.build.denied"); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := validateRunnableEndpoint(endpoint, domain.JobCapabilityDistributionBuild); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
|
||||
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentClientManager, request.ProfileKey)
|
||||
if err != nil {
|
||||
@@ -184,52 +206,12 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
|
||||
config := generatedPackageConfig{
|
||||
Kind: string(domain.DistributionComponentClientManager),
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
SecretRef: key.SecretRef,
|
||||
KeyGeneration: key.Generation,
|
||||
AuthKey: plainKey,
|
||||
}
|
||||
checkout := clientManagerCheckoutPlan{
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
SourceRevision: request.SourceRevision,
|
||||
CheckoutRef: clientManagerCheckoutRef(request.RepositoryURL, request.SourceRevision),
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
}
|
||||
outputArtifacts := []string{clientManagerOutputName(request.ProfileKey, request.TargetOS)}
|
||||
buildLogPayload := []byte(clientManagerBuildLog(checkout, config, outputArtifacts))
|
||||
buildLogArtifactID := artifactIDForDistribution(clientDistributionID + "-build-log")
|
||||
buildLogArtifact, err := svc.createPlatformArtifactPayload(buildLogArtifactID, domain.ArtifactOwnerKindServerInstance, instance.ID, buildLogPayload)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
packagePayload := generatedClientManagerPackage{
|
||||
Kind: "client-manager-package",
|
||||
Checkout: checkout,
|
||||
Config: config,
|
||||
OutputArtifacts: outputArtifacts,
|
||||
BuildLogRef: "artifact://" + buildLogArtifact.ID,
|
||||
KeyFingerprint: fingerprintForString(plainKey),
|
||||
}
|
||||
payload, err := json.MarshalIndent(packagePayload, "", " ")
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
artifactID := artifactIDForDistribution(clientDistributionID)
|
||||
artifact, err := svc.createPlatformArtifactPayload(artifactID, domain.ArtifactOwnerKindServerInstance, instance.ID, payload)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
|
||||
_ = plainKey
|
||||
artifactID := artifactIDForDistribution(clientDistributionID + "-binary")
|
||||
stamp := svc.now()
|
||||
buildJobID := jobIDFromParts("job-distribution-build", instance.ID, clientDistributionID)
|
||||
buildJob := domain.ClientManagerBuildJob{
|
||||
ID: distributionID("client-manager-build", instance.ID, request.ProfileKey, request.TargetOS, request.TargetArch, key.Generation, request.IdempotencyKey),
|
||||
ID: buildJobID,
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
@@ -237,11 +219,9 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
TargetArch: request.TargetArch,
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
SourceRevision: request.SourceRevision,
|
||||
ArtifactID: artifact.ID,
|
||||
Checksum: artifact.Checksum,
|
||||
ArtifactID: artifactID,
|
||||
KeyGeneration: key.Generation,
|
||||
LogsRef: "artifact://" + buildLogArtifact.ID,
|
||||
Status: domain.DistributionJobStatusSucceeded,
|
||||
Status: domain.DistributionJobStatusQueued,
|
||||
CreatedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
}
|
||||
@@ -249,7 +229,17 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.store.ClientManagerBuildJobs().Create(buildJob); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
if !errors.Is(err, repo.ErrDuplicate) {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
existing, getErr := svc.store.ClientManagerBuildJobs().Get(buildJob.ID)
|
||||
if getErr != nil {
|
||||
return domain.ClientManagerDistribution{}, getErr
|
||||
}
|
||||
if !sameClientManagerBuildJobArtifacts(existing, buildJob) {
|
||||
return domain.ClientManagerDistribution{}, validationError("client-manager build job already exists with different artifacts")
|
||||
}
|
||||
buildJob = existing
|
||||
}
|
||||
distribution := domain.ClientManagerDistribution{
|
||||
ID: clientDistributionID,
|
||||
@@ -261,11 +251,10 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
SourceRevision: request.SourceRevision,
|
||||
BuildJobID: buildJob.ID,
|
||||
ArtifactID: artifact.ID,
|
||||
Checksum: artifact.Checksum,
|
||||
ArtifactID: artifactID,
|
||||
KeyGeneration: key.Generation,
|
||||
SecretRef: key.SecretRef,
|
||||
Status: domain.DistributionStatusAvailable,
|
||||
Status: domain.DistributionStatusBuilding,
|
||||
CreatedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
}
|
||||
@@ -273,9 +262,38 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.store.ClientManagerDistributions().Create(distribution); err != nil {
|
||||
if errors.Is(err, repo.ErrDuplicate) {
|
||||
existing, getErr := svc.store.ClientManagerDistributions().Get(distribution.ID)
|
||||
if getErr != nil {
|
||||
return domain.ClientManagerDistribution{}, getErr
|
||||
}
|
||||
return domain.CopyClientManagerDistribution(existing), nil
|
||||
}
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.recordAuditEvent(user.ID, "client-manager.build", "server-instance", instance.ID, domain.AuditResultSuccess, "generated client-manager artifact with redacted runtime key ref"); err != nil {
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: buildJobID,
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: domain.JobCapabilityDistributionBuild,
|
||||
TargetKey: "distribution/client-manager/" + request.ProfileKey,
|
||||
InputRef: "input://distribution-build/" + distribution.ID,
|
||||
IdempotencyKey: "distribution-build:" + distribution.ID,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "build queued"},
|
||||
})
|
||||
if err != nil {
|
||||
buildJob.Status = domain.DistributionJobStatusFailed
|
||||
buildJob.UpdatedAt = svc.now()
|
||||
distribution.Status = domain.DistributionStatusFailed
|
||||
distribution.UpdatedAt = buildJob.UpdatedAt
|
||||
_ = svc.store.ClientManagerBuildJobs().Update(buildJob)
|
||||
_ = svc.store.ClientManagerDistributions().Update(distribution)
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if job.ID != buildJobID || job.Capability != domain.JobCapabilityDistributionBuild {
|
||||
return domain.ClientManagerDistribution{}, validationError("distribution build idempotency key conflicts with another job")
|
||||
}
|
||||
if err := svc.recordAuditEvent(user.ID, "client-manager.build", "server-instance", instance.ID, domain.AuditResultQueued, "queued client-manager source build with redacted runtime key ref"); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
return domain.CopyClientManagerDistribution(distribution), nil
|
||||
@@ -458,11 +476,11 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
||||
RunEndpointID: endpoint.ID,
|
||||
RunStatus: endpoint.Status,
|
||||
Actions: []domain.ServerRuntimeAction{
|
||||
runtimeAction("generate-run", "Generate run", pluginDeclares(plugin, "server.run.distribution") && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared", bindingReason)),
|
||||
runtimeAction("generate-run", "Generate run", pluginDeclares(plugin, "server.run.distribution") && endpointSupports(endpoint, domain.JobCapabilityDistributionBuild) && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !endpointSupports(endpoint, domain.JobCapabilityDistributionBuild), "run endpoint cannot build distributions", bindingReason)),
|
||||
runtimeAction("download-run", "Download run", hasAvailableRunPackage, "run package has not been generated"),
|
||||
runtimeAction("push-run-update", "Push run update", pluginDeclares(plugin, "server.run.distribution") && endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", bindingReason)),
|
||||
runtimeAction("reset-run-key", "Reset run key", pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared"),
|
||||
runtimeAction("generate-client-manager", "Generate client manager", pluginDeclares(plugin, "server.client-manager.manage") && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared", bindingReason)),
|
||||
runtimeAction("generate-client-manager", "Generate client manager", pluginDeclares(plugin, "server.client-manager.manage") && endpointSupports(endpoint, domain.JobCapabilityDistributionBuild) && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.client-manager.manage") || !endpointSupports(endpoint, domain.JobCapabilityDistributionBuild), "run endpoint cannot build distributions", bindingReason)),
|
||||
runtimeAction("download-client-manager", "Download client manager", hasAvailableClientPackage, "client-manager package has not been generated"),
|
||||
runtimeAction("reset-client-manager-key", "Reset client-manager key", pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared"),
|
||||
runtimeAction("dependencies-check", "Check dependencies", endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck) && bindingsComplete, fallbackReason(!endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck), "run endpoint cannot check dependencies", bindingReason)),
|
||||
@@ -546,6 +564,16 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
if err := svc.store.RunUpdateJobs().Create(updateJob); err != nil {
|
||||
if errors.Is(err, repo.ErrDuplicate) {
|
||||
existing, getErr := svc.store.RunUpdateJobs().Get(updateJob.ID)
|
||||
if getErr != nil {
|
||||
return domain.RunUpdateJob{}, getErr
|
||||
}
|
||||
if !sameRunUpdateJob(existing, updateJob) {
|
||||
return domain.RunUpdateJob{}, validationError("run update job already exists with different target")
|
||||
}
|
||||
return domain.CopyRunUpdateJob(existing), nil
|
||||
}
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
if err := svc.recordAuditEvent(user.ID, "run.update", "server-instance", instance.ID, domain.AuditResultQueued, "queued run self-update job with artifact checksum"); err != nil {
|
||||
@@ -793,6 +821,9 @@ func (svc *CoreService) revokeComponentDistributions(serverInstanceID string, ki
|
||||
|
||||
func (svc *CoreService) expireDistributionArtifact(artifactID string) error {
|
||||
artifact, err := svc.store.Artifacts().Get(artifactID)
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -819,14 +850,82 @@ func (svc *CoreService) createPlatformArtifactPayload(artifactID string, ownerKi
|
||||
return domain.Artifact{}, err
|
||||
}
|
||||
if err := svc.store.Artifacts().Create(artifact); err != nil {
|
||||
if !errors.Is(err, repo.ErrDuplicate) {
|
||||
return domain.Artifact{}, err
|
||||
}
|
||||
existing, getErr := svc.store.Artifacts().Get(artifact.ID)
|
||||
if getErr != nil {
|
||||
return domain.Artifact{}, getErr
|
||||
}
|
||||
if err := validateReusablePlatformArtifact(existing, artifact); err != nil {
|
||||
return domain.Artifact{}, err
|
||||
}
|
||||
if err := svc.ensureArtifactPayload(artifact.ID, payload, existing); err != nil {
|
||||
return domain.Artifact{}, err
|
||||
}
|
||||
return domain.CopyArtifact(existing), nil
|
||||
}
|
||||
if err := svc.ensureArtifactPayload(artifact.ID, payload, artifact); err != nil {
|
||||
return domain.Artifact{}, err
|
||||
}
|
||||
svc.artifactMu.Lock()
|
||||
svc.artifactPayloads[artifact.ID] = domain.CopyBytes(payload)
|
||||
svc.artifactMu.Unlock()
|
||||
return domain.CopyArtifact(artifact), nil
|
||||
}
|
||||
|
||||
func validateReusablePlatformArtifact(existing domain.Artifact, expected domain.Artifact) error {
|
||||
if existing.OwnerKind != expected.OwnerKind || existing.OwnerID != expected.OwnerID {
|
||||
return validationError("artifact already exists with different owner")
|
||||
}
|
||||
if existing.SizeBytes != expected.SizeBytes || existing.Checksum != expected.Checksum {
|
||||
return validationError("artifact already exists with different content")
|
||||
}
|
||||
if existing.State != domain.ArtifactStateAvailable {
|
||||
return validationError("artifact already exists but is not available")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ensureArtifactPayload(artifactID string, payload []byte, artifact domain.Artifact) error {
|
||||
svc.artifactMu.Lock()
|
||||
defer svc.artifactMu.Unlock()
|
||||
|
||||
if existingPayload, exists := svc.artifactPayloads[artifactID]; exists {
|
||||
if int64(len(existingPayload)) != artifact.SizeBytes || validator.BytesChecksum(existingPayload) != artifact.Checksum {
|
||||
return validationError("artifact payload does not match metadata")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if int64(len(payload)) != artifact.SizeBytes || validator.BytesChecksum(payload) != artifact.Checksum {
|
||||
return validationError("artifact payload does not match metadata")
|
||||
}
|
||||
svc.artifactPayloads[artifactID] = domain.CopyBytes(payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameClientManagerBuildJobArtifacts(existing domain.ClientManagerBuildJob, expected domain.ClientManagerBuildJob) bool {
|
||||
return existing.ServerInstanceID == expected.ServerInstanceID &&
|
||||
existing.PluginID == expected.PluginID &&
|
||||
existing.ProfileKey == expected.ProfileKey &&
|
||||
existing.TargetOS == expected.TargetOS &&
|
||||
existing.TargetArch == expected.TargetArch &&
|
||||
existing.RepositoryURL == expected.RepositoryURL &&
|
||||
existing.SourceRevision == expected.SourceRevision &&
|
||||
existing.ArtifactID == expected.ArtifactID &&
|
||||
existing.Checksum == expected.Checksum &&
|
||||
existing.KeyGeneration == expected.KeyGeneration &&
|
||||
existing.LogsRef == expected.LogsRef &&
|
||||
existing.Status == expected.Status
|
||||
}
|
||||
|
||||
func sameRunUpdateJob(existing domain.RunUpdateJob, expected domain.RunUpdateJob) bool {
|
||||
return existing.ServerInstanceID == expected.ServerInstanceID &&
|
||||
existing.RunEndpointID == expected.RunEndpointID &&
|
||||
existing.ArtifactID == expected.ArtifactID &&
|
||||
existing.Checksum == expected.Checksum &&
|
||||
existing.JobID == expected.JobID &&
|
||||
existing.IdempotencyKey == expected.IdempotencyKey &&
|
||||
existing.Status == expected.Status
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertDependencyStatus(instance domain.ServerInstance, request domain.DependencyJobRequest, state domain.DependencyState, message string) error {
|
||||
statusID := distributionID("dependency-status", instance.ID, request.ProbeKey)
|
||||
stamp := svc.now()
|
||||
|
||||
Reference in New Issue
Block a user