Remove legacy client-manager platform path

This commit is contained in:
npc0-hue
2026-09-03 16:40:05 +08:00
parent ec2462a312
commit 80cddbf19d
51 changed files with 382 additions and 4271 deletions
-9
View File
@@ -282,15 +282,6 @@ func (svc *CoreService) distributionArtifactDownloadPresentation(artifactID stri
}
}
}
clients, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{})
if err == nil {
for _, distribution := range clients {
if distribution.ArtifactID == artifactID {
format := packageFormatForTarget(distribution.TargetOS)
return distributionPackageFilename(distribution.ProfileKey, distribution.TargetOS, distribution.TargetArch, format), packageContentType(format), true
}
}
}
return "", "", false
}
File diff suppressed because it is too large Load Diff
-5
View File
@@ -389,11 +389,6 @@ func TestCoreServiceGeneratedSCUMRunRegistrationDoesNotQueueGuidedStart(t *testi
domain.JobCapabilityFilesRead,
"files.patch",
"logs.read",
domain.JobCapabilityClientManagerDeploy,
domain.JobCapabilityClientManagerControl,
domain.JobCapabilityClientManagerUpdate,
domain.JobCapabilityClientManagerRollback,
domain.JobCapabilityClientManagerUninstall,
"artifacts.read",
"artifacts.write",
"ai.invoke",
@@ -169,45 +169,6 @@ func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.D
}, 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
}
@@ -25,34 +25,6 @@ type progressDistributionBuilder struct {
payload []byte
}
type failingBuildProgressStore struct {
repo.Store
buildJobs repo.ClientManagerBuildJobRepository
}
func (store failingBuildProgressStore) ClientManagerBuildJobs() repo.ClientManagerBuildJobRepository {
return store.buildJobs
}
type failWhenDistributionJobRunningRepository struct {
repo.ClientManagerBuildJobRepository
jobs repo.JobRepository
err error
}
func (repository failWhenDistributionJobRunningRepository) List(filter domain.ClientManagerBuildJobFilter) ([]domain.ClientManagerBuildJob, error) {
jobs, err := repository.jobs.List(domain.JobFilter{ServerInstanceID: filter.ServerInstanceID})
if err != nil {
return nil, err
}
for _, job := range jobs {
if job.Capability == domain.JobCapabilityDistributionBuild && job.State == domain.JobStateRunning {
return nil, repository.err
}
}
return repository.ClientManagerBuildJobRepository.List(filter)
}
func (builder captureDistributionBuilder) Readiness() (bool, string) {
return true, ""
}
@@ -421,49 +393,6 @@ func TestCoreServiceProjectsPlatformBuilderProgressBeforeCompletion(t *testing.T
completeDistributionBuild(t, svc, distribution, nil)
}
func TestCoreServiceTerminalizesPlatformBuildWhenRunningProjectionFails(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
const leakedDetail = "sensitive-builder-token /private/platform/build/input/auth-key"
baseStore := svc.store
svc.store = failingBuildProgressStore{
Store: baseStore,
buildJobs: failWhenDistributionJobRunningRepository{
ClientManagerBuildJobRepository: baseStore.ClientManagerBuildJobs(),
jobs: baseStore.Jobs(),
err: errors.New(leakedDetail),
},
}
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "running-projection-failure-terminalizes",
})
if err != nil {
t.Fatalf("queue platform distribution: %v", err)
}
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
job, jobErr := svc.GetJob(distribution.BuildJobID)
updated, distributionErr := svc.store.RunDistributions().Get(distribution.ID)
if jobErr != nil || distributionErr != nil {
t.Fatalf("read failed build state: jobErr=%v distributionErr=%v", jobErr, distributionErr)
}
if isTerminalJobState(job.State) {
if job.State != domain.JobStateFailed || updated.Status != domain.DistributionStatusFailed {
t.Fatalf("projection failure did not end in failed state: job=%+v distribution=%+v", job, updated)
}
if strings.Contains(job.Progress.Message, "sensitive-builder-token") || strings.Contains(job.Progress.Message, "/private/") || strings.Contains(job.TerminalFingerprint, "sensitive-builder-token") {
t.Fatalf("terminal build state leaked internal failure details: %+v", job)
}
return
}
time.Sleep(time.Millisecond)
}
t.Fatal("platform build remained non-terminal after running projection failure")
}
func TestCoreServiceDiscardsBuildCompletedAfterKeyReset(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
inputs := make(chan domain.DistributionBuildInput, 1)
-119
View File
@@ -1,7 +1,6 @@
package service
import (
"errors"
"strings"
"time"
@@ -77,44 +76,6 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
}, 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,
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
}
@@ -122,21 +83,6 @@ func (svc *CoreService) projectDistributionBuildProgress(job domain.Job, stamp t
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
}
@@ -148,7 +94,6 @@ func (svc *CoreService) projectDistributionBuildResult(job domain.Job, stamp tim
return err
}
status := domain.DistributionStatusFailed
buildStatus := domain.DistributionJobStatusFailed
var artifact domain.Artifact
if job.State == domain.JobStateSucceeded {
artifactID := strings.TrimPrefix(job.ResultRef, "artifact://")
@@ -164,7 +109,6 @@ func (svc *CoreService) projectDistributionBuildResult(job domain.Job, stamp tim
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})
@@ -195,57 +139,6 @@ func (svc *CoreService) projectDistributionBuildResult(job domain.Job, stamp tim
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
}
@@ -265,18 +158,6 @@ func (svc *CoreService) validateDistributionBuildResult(job domain.Job) error {
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
}
+3 -105
View File
@@ -2,10 +2,8 @@ package service
import (
"archive/tar"
"archive/zip"
"bufio"
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/hex"
@@ -307,14 +305,7 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib
if len(binary) == 0 {
return nil, validationError("platform builder produced an empty distribution executable")
}
if input.ComponentKind == domain.DistributionComponentRun {
return binary, nil
}
configPayload, err := os.ReadFile(filepath.Join(outputDir, "config.yaml"))
if err != nil {
return nil, validationError("platform builder did not produce client-manager configuration")
}
return packageClientManagerDistribution(input.PackageFormat, outputName, binary, configPayload)
return binary, nil
}
// prepareRunSource implements the source phase of a Jenkins-style build. A
@@ -519,36 +510,8 @@ EOF
exit 0
fi
if [ "$COMPONENT_KIND" != "client-manager" ]; then
printf 'unsupported component kind\n' >&2
exit 2
fi
case "$REPOSITORY_URL" in
https://*) ;;
*) printf 'client-manager repository must use https\n' >&2; exit 2 ;;
esac
cd /workspace/build
progress 24 'git_sync: fetching client-manager source'
git init --quiet
git remote add origin "$REPOSITORY_URL"
git fetch --quiet --depth 1 origin "$SOURCE_REVISION"
git checkout --quiet --detach FETCH_HEAD
progress 36 'env_check: writing client-manager configuration'
{
printf 'server_url: "%s"\n' "$PLATFORM_URL"
printf 'server_instance_id: "%s"\n' "$SERVER_INSTANCE_ID"
printf 'scum_client_credential: "%s"\n' "$auth_key"
printf 'scum_client_name: "%s"\n' "$PROFILE_KEY"
printf 'scum_client_version: "platform-build"\n'
printf 'scum_client_machine_label: "managed-client"\n'
printf 'ftp_provider: 3\n'
} > config.yaml
progress 52 'deps_download: downloading Go modules'
go mod download
progress 76 'build_compile: compiling client-manager executable'
go build -trimpath -ldflags '-s -w' -o "/workspace/output/$OUTPUT_FILENAME" .
cp config.yaml /workspace/output/config.yaml
progress 88 'package_finalize: client-manager package inputs written'
printf 'unsupported component kind\n' >&2
exit 2
`
func (builder *DockerDistributionBuilder) containerArgs(input domain.DistributionBuildInput, sourceDir string, inputDir string, buildDir string, outputDir string, goBuildCacheDir string, goModCacheDir string, outputName string) []string {
@@ -588,71 +551,6 @@ func (builder *DockerDistributionBuilder) containerArgs(input domain.Distributio
}
}
func packageClientManagerDistribution(packageFormat string, outputName string, binary []byte, configPayload []byte) ([]byte, error) {
switch packageFormat {
case "zip":
return zipDistributionFiles(outputName, binary, configPayload)
case "tar.gz":
return tarGzipDistributionFiles(outputName, binary, configPayload)
default:
return nil, validationError("platform builder received an unsupported client-manager package format")
}
}
func zipDistributionFiles(outputName string, binary []byte, configPayload []byte) ([]byte, error) {
var buffer bytes.Buffer
writer := zip.NewWriter(&buffer)
files := []struct {
name string
mode os.FileMode
payload []byte
}{{outputName, 0o755, binary}, {"config.yaml", 0o600, configPayload}}
for _, file := range files {
header := &zip.FileHeader{Name: file.name, Method: zip.Deflate}
header.SetMode(file.mode)
header.SetModTime(time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC))
entry, err := writer.CreateHeader(header)
if err != nil {
return nil, err
}
if _, err := entry.Write(file.payload); err != nil {
return nil, err
}
}
if err := writer.Close(); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func tarGzipDistributionFiles(outputName string, binary []byte, configPayload []byte) ([]byte, error) {
var buffer bytes.Buffer
gzipWriter := gzip.NewWriter(&buffer)
gzipWriter.Header.ModTime = time.Unix(0, 0).UTC()
tarWriter := tar.NewWriter(gzipWriter)
files := []struct {
name string
mode int64
payload []byte
}{{outputName, 0o755, binary}, {"config.yaml", 0o600, configPayload}}
for _, file := range files {
header := &tar.Header{Name: file.name, Mode: file.mode, Size: int64(len(file.payload)), ModTime: time.Unix(0, 0).UTC()}
if err := tarWriter.WriteHeader(header); err != nil {
return nil, err
}
if _, err := tarWriter.Write(file.payload); err != nil {
return nil, err
}
}
if err := tarWriter.Close(); err != nil {
return nil, err
}
if err := gzipWriter.Close(); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// safeBuilderFailure keeps host paths and secret values out of reported build
// failures.
func safeBuilderFailure(output []byte, sensitiveValues ...string) string {
@@ -2,12 +2,9 @@ package service
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
@@ -384,29 +381,6 @@ func TestBuilderJobFailureMessageRedactsSensitiveValuesAndHostPaths(t *testing.T
}
}
func TestPackageClientManagerDistributionProducesProtectedArchives(t *testing.T) {
for _, packageFormat := range []string{"zip", "tar.gz"} {
t.Run(packageFormat, func(t *testing.T) {
payload, err := packageClientManagerDistribution(packageFormat, "client-manager.exe", []byte("binary"), []byte("credential: protected"))
if err != nil {
t.Fatalf("package client manager: %v", err)
}
files := readDistributionArchive(t, packageFormat, payload)
if string(files["client-manager.exe"].payload) != "binary" || files["client-manager.exe"].mode.Perm() != 0o755 {
t.Fatalf("unexpected executable archive entry: %+v", files["client-manager.exe"])
}
if string(files["config.yaml"].payload) != "credential: protected" || files["config.yaml"].mode.Perm() != 0o600 {
t.Fatalf("unexpected config archive entry: %+v", files["config.yaml"])
}
})
}
}
type distributionArchiveFile struct {
mode os.FileMode
payload []byte
}
func createBuilderSource(t *testing.T) string {
t.Helper()
sourceDir := t.TempDir()
@@ -443,55 +417,3 @@ func builderMountHostPath(t *testing.T, args []string, containerSuffix string) s
t.Fatalf("Docker arguments do not mount %s: %+v", containerSuffix, args)
return ""
}
func readDistributionArchive(t *testing.T, packageFormat string, payload []byte) map[string]distributionArchiveFile {
t.Helper()
files := map[string]distributionArchiveFile{}
switch packageFormat {
case "zip":
reader, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
if err != nil {
t.Fatalf("open zip: %v", err)
}
for _, entry := range reader.File {
body, err := entry.Open()
if err != nil {
t.Fatalf("open zip entry %s: %v", entry.Name, err)
}
content, err := io.ReadAll(body)
if closeErr := body.Close(); err == nil {
err = closeErr
}
if err != nil {
t.Fatalf("read zip entry %s: %v", entry.Name, err)
}
files[entry.Name] = distributionArchiveFile{mode: entry.Mode(), payload: content}
}
case "tar.gz":
gzipReader, err := gzip.NewReader(bytes.NewReader(payload))
if err != nil {
t.Fatalf("open gzip: %v", err)
}
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("read tar header: %v", err)
}
content, err := io.ReadAll(tarReader)
if err != nil {
t.Fatalf("read tar entry %s: %v", header.Name, err)
}
files[header.Name] = distributionArchiveFile{mode: os.FileMode(header.Mode), payload: content}
}
if err := gzipReader.Close(); err != nil {
t.Fatalf("close gzip: %v", err)
}
default:
t.Fatalf("unsupported archive format %q", packageFormat)
}
return files
}
-224
View File
@@ -115,158 +115,6 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
return domain.CopyRunDistribution(distribution), nil
}
func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID string, request domain.ClientManagerBuildRequest) (domain.ClientManagerDistribution, error) {
return domain.ClientManagerDistribution{}, legacyClientManagerUnsupported()
request = domain.CopyClientManagerBuildRequest(request)
if strings.TrimSpace(request.IdempotencyKey) == "" {
request.IdempotencyKey = "client-manager-" + request.ServerInstanceID + "-" + request.ProfileKey + "-" + request.TargetOS + "-" + request.TargetArch
}
if err := validator.ValidateClientManagerBuildRequest(request); err != nil {
return domain.ClientManagerDistribution{}, err
}
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.ClientManagerDistribution{}, err
}
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
if err != nil {
return domain.ClientManagerDistribution{}, err
}
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return domain.ClientManagerDistribution{}, err
}
if err := svc.validateDistributionPluginPermission(user.ID, plugin, instance.ID, "server.client-manager.manage", "client-manager.build.denied"); err != nil {
return domain.ClientManagerDistribution{}, err
}
if err := validatePluginTarget(plugin, request.TargetOS); err != nil {
return domain.ClientManagerDistribution{}, err
}
profile, err := findRuntimeClientManagerProfile(plugin, request.ProfileKey)
if err != nil {
return domain.ClientManagerDistribution{}, err
}
if strings.TrimSpace(request.SourceRevision) == "" {
request.SourceRevision = clientManagerProfileRevision(profile)
}
if !clientManagerProfileSupportsTarget(profile, request.TargetOS, request.TargetArch) || request.RepositoryURL != profile.RepositoryURL || !clientManagerProfileAllowsRevision(profile, request.SourceRevision) {
return domain.ClientManagerDistribution{}, validationError("client-manager build must match the declared profile repository, revision, and target")
}
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "client-manager.build.denied"); err != nil {
return domain.ClientManagerDistribution{}, err
}
if ready, reason := svc.distributionBuilderReadiness(); !ready {
return domain.ClientManagerDistribution{}, validationError(reason)
}
key, _, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentClientManager, request.ProfileKey)
if err != nil {
return domain.ClientManagerDistribution{}, err
}
clientDistributionID := distributionID("client-manager-dist", instance.ID, request.ProfileKey, request.TargetOS, request.TargetArch, key.Generation, request.IdempotencyKey)
if existing, err := svc.store.ClientManagerDistributions().Get(clientDistributionID); err == nil {
return domain.CopyClientManagerDistribution(existing), nil
} else if !errors.Is(err, repo.ErrNotFound) {
return domain.ClientManagerDistribution{}, err
}
artifactID := artifactIDForDistribution(clientDistributionID + "-binary")
stamp := svc.now()
buildJobID := jobIDFromParts("job-distribution-build", instance.ID, clientDistributionID)
buildJob := domain.ClientManagerBuildJob{
ID: buildJobID,
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
ProfileKey: request.ProfileKey,
Version: profile.Version,
TargetOS: request.TargetOS,
TargetArch: request.TargetArch,
RepositoryURL: request.RepositoryURL,
SourceRevision: request.SourceRevision,
ArtifactID: artifactID,
KeyGeneration: key.Generation,
Status: domain.DistributionJobStatusQueued,
CreatedAt: stamp,
UpdatedAt: stamp,
}
if err := validator.ValidateClientManagerBuildJob(buildJob); err != nil {
return domain.ClientManagerDistribution{}, err
}
if err := svc.store.ClientManagerBuildJobs().Create(buildJob); err != nil {
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,
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
ProfileKey: request.ProfileKey,
Version: profile.Version,
TargetOS: request.TargetOS,
TargetArch: request.TargetArch,
RepositoryURL: request.RepositoryURL,
SourceRevision: request.SourceRevision,
BuildJobID: buildJob.ID,
ArtifactID: artifactID,
KeyGeneration: key.Generation,
SecretRef: key.SecretRef,
Status: domain.DistributionStatusBuilding,
CreatedAt: stamp,
UpdatedAt: stamp,
}
if err := validator.ValidateClientManagerDistribution(distribution); err != nil {
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.ProjectClientManagerDistribution(distribution); err != nil {
return domain.ClientManagerDistribution{}, err
}
job, err := svc.CreateJob(domain.Job{
ID: buildJobID,
ServerInstanceID: instance.ID,
RunEndpointID: platformDistributionBuilderEndpointID,
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)
_ = svc.ProjectClientManagerDistribution(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")
}
svc.enqueueDistributionBuild(job)
return domain.CopyClientManagerDistribution(distribution), nil
}
func (svc *CoreService) OpenLatestRunDistributionDownloadForSession(sessionID string, serverInstanceID string) (domain.ArtifactDownloadReference, error) {
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
if err != nil {
@@ -288,33 +136,6 @@ func (svc *CoreService) OpenLatestRunDistributionDownloadForSession(sessionID st
return svc.OpenArtifactDownloadForSession(sessionID, domain.ArtifactDownloadReferenceRequest{ArtifactID: latest.ArtifactID})
}
func (svc *CoreService) OpenLatestClientManagerDistributionDownloadForSession(sessionID string, serverInstanceID string, profileKey string) (domain.ArtifactDownloadReference, error) {
return domain.ArtifactDownloadReference{}, legacyClientManagerUnsupported()
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
if err != nil {
return domain.ArtifactDownloadReference{}, err
}
filter := domain.ClientManagerDistributionFilter{ServerInstanceID: instance.ID, Status: domain.DistributionStatusAvailable}
if strings.TrimSpace(profileKey) != "" {
filter.ProfileKey = strings.TrimSpace(profileKey)
}
distributions, err := svc.store.ClientManagerDistributions().List(filter)
if err != nil {
return domain.ArtifactDownloadReference{}, err
}
var latest domain.ClientManagerDistribution
for _, distribution := range distributions {
if latest.ID == "" || distribution.KeyGeneration > latest.KeyGeneration || (distribution.KeyGeneration == latest.KeyGeneration && distribution.UpdatedAt.After(latest.UpdatedAt)) {
latest = distribution
}
}
if latest.ID == "" {
return domain.ArtifactDownloadReference{}, repo.ErrNotFound
}
return svc.OpenArtifactDownloadForSession(sessionID, domain.ArtifactDownloadReferenceRequest{ArtifactID: latest.ArtifactID})
}
func (svc *CoreService) ResetComponentKeyForSession(sessionID string, request domain.ComponentKeyResetRequest) (domain.EncryptedComponentKey, error) {
request = domain.CopyComponentKeyResetRequest(request)
if err := validator.ValidateComponentKeyResetRequest(request); err != nil {
@@ -810,27 +631,6 @@ func (svc *CoreService) revokeComponentDistributions(serverInstanceID string, ki
return err
}
}
case domain.DistributionComponentClientManager:
distributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: serverInstanceID, ProfileKey: componentKey})
if err != nil {
return err
}
for _, distribution := range distributions {
if distribution.KeyGeneration >= latestGeneration || distribution.Status == domain.DistributionStatusRevoked {
continue
}
distribution.Status = domain.DistributionStatusRevoked
distribution.UpdatedAt = svc.now()
if err := validator.ValidateClientManagerDistribution(distribution); err != nil {
return err
}
if err := svc.store.ClientManagerDistributions().Update(distribution); err != nil {
return err
}
if err := svc.expireDistributionArtifact(distribution.ArtifactID); err != nil {
return err
}
}
}
return nil
}
@@ -929,22 +729,6 @@ func (svc *CoreService) ensureArtifactPayload(artifactID string, payload []byte,
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.Version == expected.Version &&
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 (svc *CoreService) upsertDependencyStatus(instance domain.ServerInstance, request domain.DependencyJobRequest, jobID string, required bool, state domain.DependencyState, message string) error {
statusID := distributionID("dependency-status", instance.ID, request.ProbeKey)
stamp := svc.now()
@@ -1070,14 +854,6 @@ func fingerprintForString(value string) string {
return hex.EncodeToString(sum[:])[:12]
}
func clientManagerOutputName(profileKey string, targetOS string) string {
name := sanitizeIDPart(profileKey)
if targetOS == "windows" {
return name + ".exe"
}
return name
}
func minInt(a int, b int) int {
if a < b {
return a
+9 -21
View File
@@ -511,30 +511,18 @@ func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *test
}
}
func TestCoreServiceRejectsClientManagerDistributionRequests(t *testing.T) {
func TestCoreServiceRejectsClientManagerComponentRequests(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
_, err := svc.GenerateClientManagerDistributionForSession(session, domain.ClientManagerBuildRequest{
ServerInstanceID: instance.ID,
ProfileKey: "scum-client-manager",
TargetOS: "windows",
TargetArch: "amd64",
RepositoryURL: "https://github.com/F88888/scum_client.git",
SourceRevision: "main",
IdempotencyKey: "idem-client-manager",
})
if err == nil {
t.Fatal("expected client-manager distribution request to be rejected")
}
if _, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentClientManager,
ComponentKind: domain.DistributionComponentKind("client-manager"),
ComponentKey: "scum-client-manager",
}); err == nil || !strings.Contains(err.Error(), "componentKind") {
t.Fatalf("expected client-manager key reset to be rejected, got %v", err)
}
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentClientManager,
ComponentKind: domain.DistributionComponentKind("client-manager"),
ComponentKey: "scum-client-manager",
Generation: 1,
Key: "legacy-client-manager-key",
@@ -555,9 +543,9 @@ func newDistributionTestFixture(t *testing.T) (*CoreService, string, domain.Serv
)
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities,
domain.JobCapabilityRunSelfUpdate,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
)
plugin.BridgeActions = append(plugin.BridgeActions,
string(domain.PluginBridgeActionRunDistribution),
@@ -572,9 +560,9 @@ func newDistributionTestFixture(t *testing.T) (*CoreService, string, domain.Serv
}
endpoint.Capabilities = append(endpoint.Capabilities,
domain.JobCapabilityRunSelfUpdate,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
)
endpoint.Platform = "linux"
endpoint.Architecture = "amd64"
-26
View File
@@ -256,9 +256,6 @@ func (svc *CoreService) UpdateRunJobProgress(progress domain.RunJobProgress) (do
if err := svc.projectDependencyAndRunUpdateProgress(job, stamp); err != nil {
return domain.RunJobProgressResult{}, err
}
if err := svc.projectClientManagerLifecycleProgress(job, stamp); err != nil {
return domain.RunJobProgressResult{}, err
}
return domain.RunJobProgressResult{Accepted: true, Job: assignmentFromJob(job, progress.LeaseToken), ServerTime: stamp}, nil
}
@@ -347,9 +344,6 @@ func (svc *CoreService) CompleteRunJob(result domain.RunJobResult) (domain.RunJo
if err := svc.projectDependencyAndRunUpdateResult(job, stamp); err != nil {
return domain.RunJobResultResult{}, err
}
if err := svc.projectClientManagerLifecycleResult(job, stamp); err != nil {
return domain.RunJobResultResult{}, err
}
if err := svc.projectPluginOperationsJobResult(job, stamp); err != nil {
return domain.RunJobResultResult{}, err
}
@@ -425,26 +419,6 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "run.update.staged" {
return validationError("Run self-update result type is invalid")
}
case domain.JobCapabilityClientManagerDeploy:
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
return validationError("client-manager deploy result type is invalid")
}
case domain.JobCapabilityClientManagerControl:
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.control" {
return validationError("client-manager control result type is invalid")
}
case domain.JobCapabilityClientManagerUpdate:
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.updated" || result.State == domain.JobStateFailed && result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != "client-manager.rollback.restored" {
return validationError("client-manager update result type is invalid")
}
case domain.JobCapabilityClientManagerRollback:
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.rolled-back" {
return validationError("client-manager rollback result type is invalid")
}
case domain.JobCapabilityClientManagerUninstall:
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.uninstalled" {
return validationError("client-manager uninstall result type is invalid")
}
}
return nil
}
-3
View File
@@ -157,9 +157,6 @@ func runtimeBindingKeys(profiles domain.GamePluginRuntimeProfiles, profile domai
}
}
}
if profile.ClientManagerRef != "" {
add(profile.ClientManagerRef, true)
}
required := make([]string, 0, len(requiredSet))
for key := range requiredSet {
required = append(required, key)
+1 -1
View File
@@ -93,7 +93,7 @@ func TestCoreServiceSCUMGuidedDeployDispatchesPluginOwnedSupervisedStartAction(t
plugin.Version = "1.0.0"
plugin.ServerType = "scum"
plugin.Status = domain.GamePluginStatusInstalled
plugin.RequiredRunCapabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, "process.restart", "files.list", "files.patch", domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall, "artifacts.read", "artifacts.write", "ai.invoke"}
plugin.RequiredRunCapabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, "process.restart", "files.list", "files.patch", "artifacts.read", "artifacts.write", "ai.invoke"}
plugin.LifecycleActions = domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json"}
plugin.RuntimeProfiles.LifecycleProfiles = []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop}}}
requireManualSCUMRuntimeBindings(&plugin, "local")