697 lines
30 KiB
Go
697 lines
30 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/repo"
|
|
)
|
|
|
|
type generatedPackageConfig struct {
|
|
Kind string
|
|
ServerInstanceID string
|
|
PluginID string
|
|
RunEndpointID string
|
|
ProfileKey string
|
|
TargetOS string
|
|
TargetArch string
|
|
SecretRef string
|
|
KeyGeneration int
|
|
AuthKey string
|
|
}
|
|
|
|
func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-generate",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution: %v", err)
|
|
}
|
|
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" || distribution.Checksum != "" {
|
|
t.Fatalf("unexpected run distribution: %+v", distribution)
|
|
}
|
|
job, err := svc.GetJob(distribution.BuildJobID)
|
|
if err != nil || job.Capability != domain.JobCapabilityDistributionBuild || job.State != domain.JobStateQueued {
|
|
t.Fatalf("expected queued backend build job, job=%+v err=%v", job, err)
|
|
}
|
|
if _, err := svc.GetArtifact(distribution.ArtifactID); !errors.Is(err, repo.ErrNotFound) {
|
|
t.Fatalf("generation must not publish config JSON as an artifact, got %v", err)
|
|
}
|
|
|
|
keys, err := svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
Status: domain.ComponentKeyStatusActive,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list component keys: %v", err)
|
|
}
|
|
if len(keys) != 1 || keys[0].Generation != 1 || !strings.HasPrefix(keys[0].EncryptedKey, "enc:v1:") {
|
|
t.Fatalf("expected one active encrypted run key, got %+v", keys)
|
|
}
|
|
|
|
config := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
|
if config.AuthKey == "" || config.AuthKey == keys[0].EncryptedKey || strings.Contains(distribution.SecretRef, config.AuthKey) {
|
|
t.Fatalf("run package key leaked through metadata or was not encrypted, config=%+v key=%+v distribution=%+v", config, keys[0], distribution)
|
|
}
|
|
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
Generation: config.KeyGeneration,
|
|
Key: config.AuthKey,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("authenticate run: %v", err)
|
|
}
|
|
if !auth.Allowed {
|
|
t.Fatalf("expected current run key to authenticate, got %+v", auth)
|
|
}
|
|
|
|
second, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-generate-second",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate second run distribution: %v", err)
|
|
}
|
|
if second.KeyGeneration != 1 || second.SecretRef != distribution.SecretRef {
|
|
t.Fatalf("expected second package to reuse current singleton key, got first=%+v second=%+v", distribution, second)
|
|
}
|
|
keys, err = svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
Status: domain.ComponentKeyStatusActive,
|
|
})
|
|
if err != nil || len(keys) != 1 {
|
|
t.Fatalf("expected one active key after second generation, keys=%+v err=%v", keys, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRejectsDistributionBuildForStaleRunEndpoint(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
svc.now = func() time.Time { return fixedTime.Add(capacityHeartbeatStaleAfter + time.Second) }
|
|
|
|
_, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-stale-run-endpoint",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "heartbeat is stale") {
|
|
t.Fatalf("expected stale Run endpoint rejection, got %v", err)
|
|
}
|
|
|
|
actions, err := svc.GetServerRuntimeActionsForSession(session, instance.ID)
|
|
if err != nil {
|
|
t.Fatalf("get runtime actions: %v", err)
|
|
}
|
|
for _, action := range actions.Actions {
|
|
if action.Key == "generate-run" && action.Available {
|
|
t.Fatalf("stale Run endpoint must not expose generate-run as available: %+v", action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRuntimeActionsGateDependenciesOnPluginPermission(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
|
if err != nil {
|
|
t.Fatalf("get plugin: %v", err)
|
|
}
|
|
permissions := plugin.DeclaredPermissions[:0]
|
|
for _, permission := range plugin.DeclaredPermissions {
|
|
if permission != "server.dependencies.manage" {
|
|
permissions = append(permissions, permission)
|
|
}
|
|
}
|
|
plugin.DeclaredPermissions = permissions
|
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
|
t.Fatalf("update plugin: %v", err)
|
|
}
|
|
|
|
actions, err := svc.GetServerRuntimeActionsForSession(session, instance.ID)
|
|
if err != nil {
|
|
t.Fatalf("get runtime actions: %v", err)
|
|
}
|
|
seen := 0
|
|
for _, action := range actions.Actions {
|
|
if action.Key != "dependencies-check" && action.Key != "dependencies-install" {
|
|
continue
|
|
}
|
|
seen++
|
|
if action.Available || action.Reason != "plugin permission is not declared" {
|
|
t.Fatalf("expected dependency action gated by plugin permission, got %+v", action)
|
|
}
|
|
}
|
|
if seen != 2 {
|
|
t.Fatalf("expected both dependency actions in %+v", actions.Actions)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceDistributionBuildRejectsPrematureSuccessAndCanRetryAfterUpload(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-premature-result",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution: %v", err)
|
|
}
|
|
|
|
helloRequest := validRunControlHello()
|
|
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityDistributionBuild)
|
|
hello, err := svc.RegisterRunHello(helloRequest)
|
|
if err != nil {
|
|
t.Fatalf("register build worker: %v", err)
|
|
}
|
|
claim, err := svc.ClaimRunJob(domain.RunJobClaim{
|
|
RunEndpointID: instance.RunEndpointID,
|
|
SessionToken: hello.SessionToken,
|
|
Capabilities: []string{domain.JobCapabilityDistributionBuild},
|
|
Capacity: domain.RunCapacity{MaxJobs: 1},
|
|
})
|
|
if err != nil || !claim.HasJob || claim.Job.JobID != distribution.BuildJobID {
|
|
t.Fatalf("claim distribution build job: claim=%+v err=%v", claim, err)
|
|
}
|
|
result := domain.RunJobResult{
|
|
RunEndpointID: instance.RunEndpointID,
|
|
SessionToken: hello.SessionToken,
|
|
JobID: claim.Job.JobID,
|
|
LeaseToken: claim.Job.LeaseToken,
|
|
Attempt: claim.Job.Attempt,
|
|
State: domain.JobStateSucceeded,
|
|
Progress: domain.RunJobProgressReport{Percent: 100, Message: "package_finalize: done"},
|
|
ResultRef: "artifact://" + distribution.ArtifactID,
|
|
Message: "done",
|
|
}
|
|
if _, err := svc.CompleteRunJob(result); err == nil {
|
|
t.Fatal("expected premature success without uploaded artifact to be rejected")
|
|
}
|
|
stored, err := svc.GetJob(distribution.BuildJobID)
|
|
if err != nil || stored.State != domain.JobStateAccepted {
|
|
t.Fatalf("premature success must not make the job terminal, job=%+v err=%v", stored, err)
|
|
}
|
|
|
|
if _, err := svc.createPlatformArtifactPayload(distribution.ArtifactID, domain.ArtifactOwnerKindJob, distribution.BuildJobID, []byte("actual compiled archive")); err != nil {
|
|
t.Fatalf("publish uploaded build output: %v", err)
|
|
}
|
|
if _, err := svc.CompleteRunJob(result); err != nil {
|
|
t.Fatalf("retry success after artifact upload: %v", err)
|
|
}
|
|
stored, err = svc.GetJob(distribution.BuildJobID)
|
|
if err != nil || stored.State != domain.JobStateSucceeded {
|
|
t.Fatalf("expected terminal success after upload, job=%+v err=%v", stored, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRunDistributionDownloadUsesTargetPackageName(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "windows",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-download-name",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution: %v", err)
|
|
}
|
|
payload := []byte("windows zipped run package")
|
|
completeDistributionBuild(t, svc, distribution, payload)
|
|
|
|
reference, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: distribution.ArtifactID})
|
|
if err != nil {
|
|
t.Fatalf("open run artifact download: %v", err)
|
|
}
|
|
if reference.Filename != "run-windows-amd64.zip" || reference.ContentType != "application/zip" {
|
|
t.Fatalf("expected windows run package metadata, got %+v", reference)
|
|
}
|
|
|
|
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: distribution.ArtifactID, Limit: len(payload)})
|
|
if err != nil {
|
|
t.Fatalf("read run artifact content: %v", err)
|
|
}
|
|
if content.Filename != reference.Filename || content.ContentType != reference.ContentType {
|
|
t.Fatalf("expected content metadata to match reference, reference=%+v content=%+v", reference, content)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRunDistributionRetryReusesPartialArtifact(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
|
if err != nil {
|
|
t.Fatalf("ensure run key: %v", err)
|
|
}
|
|
|
|
idempotencyKey := "web:run.generate:scum-alpha:1784043453685"
|
|
distributionID := distributionID("run-dist", instance.ID, "windows", "amd64", key.Generation, idempotencyKey)
|
|
artifactID := artifactIDForDistribution(distributionID)
|
|
payload := []byte("legacy generated config is not a binary distribution")
|
|
partialArtifact, err := svc.createPlatformArtifactPayload(artifactID, domain.ArtifactOwnerKindServerInstance, instance.ID, payload)
|
|
if err != nil {
|
|
t.Fatalf("create partial artifact: %v", err)
|
|
}
|
|
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "windows",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: idempotencyKey,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("retry run generation should recover partial artifact: %v", err)
|
|
}
|
|
if distribution.ID != distributionID || distribution.ArtifactID == partialArtifact.ID || distribution.Checksum != "" {
|
|
t.Fatalf("expected real binary build to ignore legacy config artifact, distribution=%+v artifact=%+v", distribution, partialArtifact)
|
|
}
|
|
if distribution.PackageFormat != "zip" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" {
|
|
t.Fatalf("unexpected recovered distribution: %+v", distribution)
|
|
}
|
|
recoveredConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
|
if recoveredConfig.AuthKey != plainKey || recoveredConfig.KeyGeneration != key.Generation {
|
|
t.Fatalf("expected recovered artifact payload to remain downloadable, got %+v", recoveredConfig)
|
|
}
|
|
}
|
|
|
|
func TestCoreServicePushRunUpdateReusesExistingUpdateJob(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-before-update",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution: %v", err)
|
|
}
|
|
distribution = completeDistributionBuild(t, svc, distribution, []byte("compiled run archive"))
|
|
|
|
request := domain.RunUpdateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ArtifactID: distribution.ArtifactID,
|
|
Checksum: distribution.Checksum,
|
|
IdempotencyKey: "idem-run-update-repeat",
|
|
}
|
|
first, err := svc.PushRunUpdateForSession(session, request)
|
|
if err != nil {
|
|
t.Fatalf("push run update: %v", err)
|
|
}
|
|
second, err := svc.PushRunUpdateForSession(session, request)
|
|
if err != nil {
|
|
t.Fatalf("push run update again should reuse existing update job: %v", err)
|
|
}
|
|
if second.ID != first.ID || second.JobID != first.JobID || second.ArtifactID != first.ArtifactID {
|
|
t.Fatalf("expected repeated push to return existing update job, first=%+v second=%+v", first, second)
|
|
}
|
|
if second.Status != domain.DistributionJobStatusQueued {
|
|
t.Fatalf("expected queued existing update job, got %+v", second)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-before-reset",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution: %v", err)
|
|
}
|
|
oldConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
|
distribution = completeDistributionBuild(t, svc, distribution, []byte("compiled run archive before reset"))
|
|
|
|
reset, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("reset run key: %v", err)
|
|
}
|
|
if reset.Generation != 2 || reset.Status != domain.ComponentKeyStatusActive {
|
|
t.Fatalf("expected reset key generation 2, got %+v", reset)
|
|
}
|
|
oldDistribution, err := svc.store.RunDistributions().Get(distribution.ID)
|
|
if err != nil {
|
|
t.Fatalf("get old distribution: %v", err)
|
|
}
|
|
if oldDistribution.Status != domain.DistributionStatusRevoked {
|
|
t.Fatalf("expected old distribution revoked, got %+v", oldDistribution)
|
|
}
|
|
if _, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: distribution.ArtifactID}); err == nil || !strings.Contains(err.Error(), "available") {
|
|
t.Fatalf("expected old artifact download to be unavailable, got %v", err)
|
|
}
|
|
|
|
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
Generation: oldConfig.KeyGeneration,
|
|
Key: oldConfig.AuthKey,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("authenticate old key: %v", err)
|
|
}
|
|
if auth.Allowed || !strings.Contains(auth.Reason, "generation") {
|
|
t.Fatalf("expected old package authentication denial, got %+v", auth)
|
|
}
|
|
|
|
newDistribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-after-reset",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution after reset: %v", err)
|
|
}
|
|
newConfig := readGeneratedPackageConfig(t, svc, session, newDistribution.ArtifactID)
|
|
if newDistribution.KeyGeneration != 2 || newConfig.AuthKey == oldConfig.AuthKey {
|
|
t.Fatalf("expected regenerated package with new generation/key, old=%+v new=%+v", oldConfig, newConfig)
|
|
}
|
|
auth, err = svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
Generation: newConfig.KeyGeneration,
|
|
Key: newConfig.AuthKey,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("authenticate new key: %v", err)
|
|
}
|
|
if !auth.Allowed {
|
|
t.Fatalf("expected regenerated package to authenticate, got %+v", auth)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceBuildsClientManagerWithDistinctKeyAndAuditsSensitiveOperations(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
runDistribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-for-client",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution: %v", err)
|
|
}
|
|
runDistribution = completeDistributionBuild(t, svc, runDistribution, []byte("compiled run archive"))
|
|
if _, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: runDistribution.ArtifactID}); err != nil {
|
|
t.Fatalf("open run download: %v", err)
|
|
}
|
|
runConfig := readGeneratedPackageConfig(t, svc, session, runDistribution.ArtifactID)
|
|
|
|
clientDistribution, 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.Fatalf("generate client-manager distribution: %v", err)
|
|
}
|
|
clientConfig := readGeneratedPackageConfig(t, svc, session, clientDistribution.ArtifactID)
|
|
if clientDistribution.KeyGeneration != 1 || clientDistribution.BuildJobID == "" || clientDistribution.Status != domain.DistributionStatusBuilding {
|
|
t.Fatalf("unexpected client-manager distribution: %+v", clientDistribution)
|
|
}
|
|
if clientConfig.AuthKey == runConfig.AuthKey || clientDistribution.SecretRef == runDistribution.SecretRef {
|
|
t.Fatalf("client-manager must use a distinct key/ref, run=%+v client=%+v", runConfig, clientConfig)
|
|
}
|
|
build, err := svc.store.ClientManagerBuildJobs().Get(clientDistribution.BuildJobID)
|
|
if err != nil {
|
|
t.Fatalf("get build job: %v", err)
|
|
}
|
|
if build.Status != domain.DistributionJobStatusQueued || build.RepositoryURL != "https://github.com/F88888/scum_client.git" || build.SourceRevision != "main" {
|
|
t.Fatalf("unexpected build job: %+v", build)
|
|
}
|
|
clientDistribution = completeClientDistributionBuild(t, svc, clientDistribution, []byte("compiled client archive"))
|
|
build, err = svc.store.ClientManagerBuildJobs().Get(clientDistribution.BuildJobID)
|
|
if err != nil || build.Status != domain.DistributionJobStatusSucceeded || build.Checksum == "" {
|
|
t.Fatalf("expected uploaded client build to project as succeeded, build=%+v err=%v", build, err)
|
|
}
|
|
|
|
_, err = svc.GenerateClientManagerDistributionForSession(session, domain.ClientManagerBuildRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ProfileKey: "scum-client-manager",
|
|
TargetOS: "darwin",
|
|
TargetArch: "amd64",
|
|
RepositoryURL: "https://github.com/F88888/scum_client.git",
|
|
IdempotencyKey: "idem-client-manager-denied",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "targetOs") {
|
|
t.Fatalf("expected unsupported target denial, got %v", err)
|
|
}
|
|
|
|
if _, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentClientManager,
|
|
ComponentKey: "scum-client-manager",
|
|
}); err != nil {
|
|
t.Fatalf("reset client-manager key: %v", err)
|
|
}
|
|
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentClientManager,
|
|
ComponentKey: "scum-client-manager",
|
|
Generation: clientConfig.KeyGeneration,
|
|
Key: clientConfig.AuthKey,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("authenticate old client key: %v", err)
|
|
}
|
|
if auth.Allowed {
|
|
t.Fatalf("expected old client-manager key to be denied after reset, got %+v", auth)
|
|
}
|
|
|
|
audits, err := svc.ListAuditEvents(domain.AuditEventFilter{ResourceID: instance.ID})
|
|
if err != nil {
|
|
t.Fatalf("list audits: %v", err)
|
|
}
|
|
actions := map[string]bool{}
|
|
for _, audit := range audits {
|
|
actions[audit.Action] = true
|
|
for _, forbidden := range []string{runConfig.AuthKey, clientConfig.AuthKey, "password=", "unix://", "/Users/"} {
|
|
if strings.Contains(audit.Summary, forbidden) {
|
|
t.Fatalf("audit leaked forbidden fragment %q in %+v", forbidden, audit)
|
|
}
|
|
}
|
|
}
|
|
for _, action := range []string{"run.generate", "run.download", "client-manager.build", "client-manager.build.denied", "runtime-key.reset"} {
|
|
if !actions[action] {
|
|
t.Fatalf("expected audit action %q in %+v", action, audits)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newDistributionTestFixture(t *testing.T) (*CoreService, string, domain.ServerInstance) {
|
|
t.Helper()
|
|
svc := newTestCoreService()
|
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
|
plugin.SupportedOS = []string{"linux", "windows"}
|
|
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions,
|
|
"server.run.distribution",
|
|
"server.client-manager.manage",
|
|
"server.dependencies.manage",
|
|
)
|
|
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities,
|
|
domain.JobCapabilityRunSelfUpdate,
|
|
domain.JobCapabilityDependenciesCheck,
|
|
domain.JobCapabilityDependenciesInstall,
|
|
domain.JobCapabilityLogsBackfill,
|
|
domain.JobCapabilityClientManagerDeploy,
|
|
domain.JobCapabilityClientManagerControl,
|
|
domain.JobCapabilityClientManagerUpdate,
|
|
domain.JobCapabilityClientManagerRollback,
|
|
domain.JobCapabilityClientManagerUninstall,
|
|
)
|
|
plugin.BridgeActions = append(plugin.BridgeActions,
|
|
string(domain.PluginBridgeActionRunDistribution),
|
|
string(domain.PluginBridgeActionClientManager),
|
|
string(domain.PluginBridgeActionDependenciesRequest),
|
|
string(domain.PluginBridgeActionLogsBackfillRequest),
|
|
)
|
|
plugin.RuntimeProfiles.DependencyProbes = []domain.RuntimeDependencyProbe{{Key: "java-runtime", Kind: "command.version", TargetKey: "java", Platforms: []string{"linux"}}}
|
|
plugin.RuntimeProfiles.InstallPlans = []domain.RuntimeInstallPlan{{Key: "java-install", Title: "Install Java", Platforms: []string{"linux"}, Steps: []domain.RuntimeInstallStep{{Type: "package", TargetKey: "java", PackageManager: "apt", PackageName: "openjdk-21-jre"}}}}
|
|
plugin.RuntimeProfiles.ClientManagers = []domain.RuntimeClientManagerProfile{{Key: "scum-client-manager", DisplayName: "SCUM Client Manager", Version: "1.0.0", RepositoryURL: "https://github.com/F88888/scum_client.git", RevisionPolicy: "branch", Branch: "main", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}, {OS: "linux", Arch: "amd64"}}, BuildSystem: "go", EntryRef: "main.go", OutputArtifacts: []string{"scum_client.exe"}, Deployment: domain.RuntimeClientManagerDeployment{Mode: "run-supervised", ExecutableRef: "scum_client.exe", RequiredRunCapabilities: []string{domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall}}, Lifecycle: domain.RuntimeClientManagerLifecycle{Actions: []string{"start", "stop", "restart", "status", "update", "rollback", "uninstall"}, StartupTimeoutSeconds: 60, StopTimeoutSeconds: 30}, Health: domain.RuntimeClientManagerHealth{Mode: "component-heartbeat", IntervalSeconds: 15, DegradedAfterSeconds: 45, OfflineAfterSeconds: 120, RequiredCapabilities: []string{"component.register", "component.heartbeat", "component.health"}}, Compatibility: domain.RuntimeClientManagerCompatibility{MinimumVersion: "1.0.0"}, UpdatePolicy: domain.RuntimeClientManagerUpdatePolicy{Strategy: "manual-staged", RequireApproval: true, HealthConfirmationSeconds: 60, RetainPrevious: true}}}
|
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
|
t.Fatalf("update plugin fixture: %v", err)
|
|
}
|
|
endpoint.Capabilities = append(endpoint.Capabilities,
|
|
domain.JobCapabilityDistributionBuild,
|
|
domain.JobCapabilityRunSelfUpdate,
|
|
domain.JobCapabilityDependenciesCheck,
|
|
domain.JobCapabilityDependenciesInstall,
|
|
domain.JobCapabilityLogsBackfill,
|
|
domain.JobCapabilityClientManagerDeploy,
|
|
domain.JobCapabilityClientManagerControl,
|
|
domain.JobCapabilityClientManagerUpdate,
|
|
domain.JobCapabilityClientManagerRollback,
|
|
domain.JobCapabilityClientManagerUninstall,
|
|
)
|
|
endpoint.Platform = "linux"
|
|
endpoint.Architecture = "amd64"
|
|
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
|
t.Fatalf("update endpoint fixture: %v", err)
|
|
}
|
|
session := createServiceUserAndLogin(t, svc, domain.User{
|
|
ID: "user-distribution-owner",
|
|
DisplayName: "Distribution Owner",
|
|
Email: "distribution-owner@example.test",
|
|
Roles: []string{"server-owner"},
|
|
PasswordHash: "secret-password",
|
|
})
|
|
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
|
|
ID: "server-distribution",
|
|
PluginID: plugin.ID,
|
|
RunEndpointID: endpoint.ID,
|
|
Name: "Distribution Server",
|
|
State: domain.ServerInstanceStateReady,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create distribution server: %v", err)
|
|
}
|
|
createCompleteRuntimeBinding(t, svc, instance, "local")
|
|
return svc, session, instance
|
|
}
|
|
|
|
func readGeneratedPackageConfig(t *testing.T, svc *CoreService, session string, artifactID string) generatedPackageConfig {
|
|
t.Helper()
|
|
_ = session
|
|
runs, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{})
|
|
if err != nil {
|
|
t.Fatalf("list run distributions: %v", err)
|
|
}
|
|
for _, distribution := range runs {
|
|
if distribution.ArtifactID != artifactID {
|
|
continue
|
|
}
|
|
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentRun, "")
|
|
if err != nil {
|
|
t.Fatalf("get run key: %v", err)
|
|
}
|
|
plain, err := decryptRuntimeKey(key.EncryptedKey)
|
|
if err != nil {
|
|
t.Fatalf("decrypt run key: %v", err)
|
|
}
|
|
return generatedPackageConfig{Kind: "run", ServerInstanceID: distribution.ServerInstanceID, PluginID: distribution.PluginID, RunEndpointID: distribution.RunEndpointID, TargetOS: distribution.TargetOS, TargetArch: distribution.TargetArch, SecretRef: distribution.SecretRef, KeyGeneration: distribution.KeyGeneration, AuthKey: plain}
|
|
}
|
|
clients, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{})
|
|
if err != nil {
|
|
t.Fatalf("list client distributions: %v", err)
|
|
}
|
|
for _, distribution := range clients {
|
|
if distribution.ArtifactID != artifactID {
|
|
continue
|
|
}
|
|
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentClientManager, distribution.ProfileKey)
|
|
if err != nil {
|
|
t.Fatalf("get client key: %v", err)
|
|
}
|
|
plain, err := decryptRuntimeKey(key.EncryptedKey)
|
|
if err != nil {
|
|
t.Fatalf("decrypt client key: %v", err)
|
|
}
|
|
return generatedPackageConfig{Kind: "client-manager", ServerInstanceID: distribution.ServerInstanceID, PluginID: distribution.PluginID, ProfileKey: distribution.ProfileKey, TargetOS: distribution.TargetOS, TargetArch: distribution.TargetArch, SecretRef: distribution.SecretRef, KeyGeneration: distribution.KeyGeneration, AuthKey: plain}
|
|
}
|
|
t.Fatalf("distribution for artifact %s was not found", artifactID)
|
|
return generatedPackageConfig{}
|
|
}
|
|
|
|
func completeDistributionBuild(t *testing.T, svc *CoreService, distribution domain.RunDistribution, payload []byte) domain.RunDistribution {
|
|
t.Helper()
|
|
artifact, err := svc.createPlatformArtifactPayload(distribution.ArtifactID, domain.ArtifactOwnerKindJob, distribution.BuildJobID, payload)
|
|
if err != nil {
|
|
t.Fatalf("publish run build artifact: %v", err)
|
|
}
|
|
job, err := svc.GetJob(distribution.BuildJobID)
|
|
if err != nil {
|
|
t.Fatalf("get run build job: %v", err)
|
|
}
|
|
job.State = domain.JobStateSucceeded
|
|
job.Progress = domain.JobProgress{Percent: 100, Message: "package_finalize: build artifact available"}
|
|
job.ResultRef = "artifact://" + artifact.ID
|
|
job.UpdatedAt = svc.now()
|
|
if err := svc.store.Jobs().Update(job); err != nil {
|
|
t.Fatalf("update run build job: %v", err)
|
|
}
|
|
if err := svc.projectDistributionBuildResult(job, svc.now()); err != nil {
|
|
t.Fatalf("project run build: %v", err)
|
|
}
|
|
updated, err := svc.store.RunDistributions().Get(distribution.ID)
|
|
if err != nil {
|
|
t.Fatalf("get completed run distribution: %v", err)
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func completeClientDistributionBuild(t *testing.T, svc *CoreService, distribution domain.ClientManagerDistribution, payload []byte) domain.ClientManagerDistribution {
|
|
t.Helper()
|
|
artifact, err := svc.createPlatformArtifactPayload(distribution.ArtifactID, domain.ArtifactOwnerKindJob, distribution.BuildJobID, payload)
|
|
if err != nil {
|
|
t.Fatalf("publish client build artifact: %v", err)
|
|
}
|
|
job, err := svc.GetJob(distribution.BuildJobID)
|
|
if err != nil {
|
|
t.Fatalf("get client build job: %v", err)
|
|
}
|
|
job.State = domain.JobStateSucceeded
|
|
job.Progress = domain.JobProgress{Percent: 100, Message: "package_finalize: build artifact available"}
|
|
job.ResultRef = "artifact://" + artifact.ID
|
|
job.UpdatedAt = svc.now()
|
|
if err := svc.store.Jobs().Update(job); err != nil {
|
|
t.Fatalf("update client build job: %v", err)
|
|
}
|
|
if err := svc.projectDistributionBuildResult(job, svc.now()); err != nil {
|
|
t.Fatalf("project client build: %v", err)
|
|
}
|
|
updated, err := svc.store.ClientManagerDistributions().Get(distribution.ID)
|
|
if err != nil {
|
|
t.Fatalf("get completed client distribution: %v", err)
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func TestCoreServiceDeniesRunDistributionWithoutPluginDeclaration(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
|
session := createServiceUserAndLogin(t, svc, domain.User{
|
|
ID: "user-distribution-denied",
|
|
DisplayName: "Distribution Denied",
|
|
Email: "distribution-denied@example.test",
|
|
Roles: []string{"server-owner"},
|
|
PasswordHash: "secret-password",
|
|
})
|
|
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
|
|
ID: "server-distribution-denied",
|
|
PluginID: plugin.ID,
|
|
RunEndpointID: endpoint.ID,
|
|
Name: "Distribution Denied Server",
|
|
State: domain.ServerInstanceStateReady,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create denied server: %v", err)
|
|
}
|
|
_, err = svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-run-denied",
|
|
})
|
|
if !errors.Is(err, ErrForbidden) {
|
|
t.Fatalf("expected plugin declaration denial, got %v", err)
|
|
}
|
|
}
|