Make generated run lifecycle autonomous
This commit is contained in:
@@ -111,12 +111,6 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
svc.runSessions[hello.RunEndpointID] = session
|
||||
if err := svc.queueManagedGuidedDeploymentAfterRegistration(hello); err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
if err := svc.queueRuntimeStateReconciliationAfterRegistration(hello, generation); err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
featureFlags := []string{"control.hello", "control.heartbeat", "signed-envelope.v1.optional"}
|
||||
if session.RequireSignedRequests {
|
||||
featureFlags[2] = "signed-envelope.v1.required"
|
||||
@@ -132,96 +126,6 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
}), nil
|
||||
}
|
||||
|
||||
// queueRuntimeStateReconciliationAfterRegistration lets a generated Run correct
|
||||
// stale observed state after reconnecting. Platform still owns command dispatch,
|
||||
// but the Run-owned process supervisor is authoritative for whether the local
|
||||
// managed process exists.
|
||||
func (svc *CoreService) queueRuntimeStateReconciliationAfterRegistration(hello domain.RunControlHello, generation int) error {
|
||||
if hello.ComponentKind != domain.DistributionComponentRun || strings.TrimSpace(hello.ServerInstanceID) == "" {
|
||||
return nil
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(hello.ServerInstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if instance.RunEndpointID != hello.RunEndpointID || instance.RunEndpointID != dedicatedRunEndpointID(instance.ID) {
|
||||
return nil
|
||||
}
|
||||
if instance.State != domain.ServerInstanceStateRunning && instance.State != domain.ServerInstanceStateFailed {
|
||||
return nil
|
||||
}
|
||||
if !containsString(hello.CapabilityReport.Capabilities, domain.LifecycleCapabilityStatus) {
|
||||
return nil
|
||||
}
|
||||
active, err := svc.hasActiveServerLifecycleJob(instance.ID)
|
||||
if err != nil || active {
|
||||
return err
|
||||
}
|
||||
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionStatus); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, domain.ServerLifecycleActionStatus); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityStatus); err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = svc.dispatchLifecycleJob(instance, domain.ServerLifecycleActionStatus, fmt.Sprintf("runtime-state-reconcile:%s:g%d", instance.ID, generation))
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc *CoreService) hasActiveServerLifecycleJob(serverInstanceID string) (bool, error) {
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: serverInstanceID})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if !isLifecycleJobCapability(job.Capability) {
|
||||
continue
|
||||
}
|
||||
if job.State == domain.JobStateQueued || job.State == domain.JobStateAccepted || job.State == domain.JobStateRunning || job.State == domain.JobStateRetrying {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isLifecycleJobCapability(capability string) bool {
|
||||
switch capability {
|
||||
case domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// queueManagedGuidedDeploymentAfterRegistration advances only a newly-created,
|
||||
// dedicated guided server. Selecting guided-install is the owner's prior
|
||||
// authorization for the plugin-declared bootstrap action; reconnects remain
|
||||
// idempotent.
|
||||
func (svc *CoreService) queueManagedGuidedDeploymentAfterRegistration(hello domain.RunControlHello) error {
|
||||
if hello.ComponentKind != domain.DistributionComponentRun || strings.TrimSpace(hello.ServerInstanceID) == "" {
|
||||
return nil
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(hello.ServerInstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if instance.RunEndpointID != hello.RunEndpointID || instance.RunEndpointID != dedicatedRunEndpointID(instance.ID) || instance.State != domain.ServerInstanceStateDraft || instance.Deployment.Mode != domain.ServerDeploymentModeGuided {
|
||||
return nil
|
||||
}
|
||||
_, err = svc.deployServerInstance(domain.ServerLifecycleCommand{
|
||||
ServerInstanceID: instance.ID,
|
||||
ExpectedConfigVersion: instance.ConfigVersion,
|
||||
IdempotencyKey: fmt.Sprintf("managed-deploy:%s:r%d", instance.ID, instance.Deployment.Revision),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc *CoreService) validateDedicatedRunHello(hello domain.RunControlHello) error {
|
||||
if hello.ComponentKind != domain.DistributionComponentRun {
|
||||
return validationError("component-authenticated run hello must use the run component")
|
||||
|
||||
@@ -268,7 +268,7 @@ func TestCoreServiceComponentRunCannotClaimDistributionBuild(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceDedicatedRunRegistrationAutomaticallyDeploysGuidedDraftOnly(t *testing.T) {
|
||||
func TestCoreServiceDedicatedRunRegistrationDoesNotDispatchLifecycleBootstrap(t *testing.T) {
|
||||
svc, _ := newLifecycleRunService(t)
|
||||
plugin := createLifecyclePlugin(t, svc)
|
||||
endpoint, err := svc.store.RunEndpoints().Get("run-local")
|
||||
@@ -290,17 +290,17 @@ func TestCoreServiceDedicatedRunRegistrationAutomaticallyDeploysGuidedDraftOnly(
|
||||
}
|
||||
registerDedicatedRunForTest(t, svc, guided.Instance, plugin.ID)
|
||||
stored, err := svc.GetServerInstance(guided.Instance.ID)
|
||||
if err != nil || stored.State != domain.ServerInstanceStateInstalling {
|
||||
t.Fatalf("guided registration should queue bootstrap start, server=%+v err=%v", stored, err)
|
||||
if err != nil || stored.State != domain.ServerInstanceStateDraft {
|
||||
t.Fatalf("guided registration must leave lifecycle authority with Run, server=%+v err=%v", stored, err)
|
||||
}
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: guided.Instance.ID})
|
||||
if err != nil || len(jobs) != 1 || jobs[0].Capability != domain.LifecycleCapabilityStart || jobs[0].TargetKey != "actions/start.json" || len(jobs[0].ExecutionInput.LogSources) != 2 {
|
||||
t.Fatalf("expected one automatic supervised start job, jobs=%+v err=%v", jobs, err)
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("registration must not enqueue automatic lifecycle jobs, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
registerDedicatedRunForTest(t, svc, guided.Instance, plugin.ID)
|
||||
jobs, _ = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: guided.Instance.ID})
|
||||
if len(jobs) != 1 {
|
||||
t.Fatalf("Run reconnect must not duplicate automatic bootstrap, jobs=%+v", jobs)
|
||||
if len(jobs) != 0 {
|
||||
t.Fatalf("Run reconnect must not enqueue bootstrap jobs, jobs=%+v", jobs)
|
||||
}
|
||||
|
||||
generatedRunDraft := domain.ServerInstance{
|
||||
@@ -318,12 +318,12 @@ func TestCoreServiceDedicatedRunRegistrationAutomaticallyDeploysGuidedDraftOnly(
|
||||
}
|
||||
registerDedicatedRunForTest(t, svc, generatedRunDraft, plugin.ID)
|
||||
storedGenerated, err := svc.GetServerInstance(generatedRunDraft.ID)
|
||||
if err != nil || storedGenerated.State != domain.ServerInstanceStateInstalling {
|
||||
t.Fatalf("generated Run registration should queue supervised start without deployment target, server=%+v err=%v", storedGenerated, err)
|
||||
if err != nil || storedGenerated.State != domain.ServerInstanceStateDraft {
|
||||
t.Fatalf("generated Run registration must not platform-dispatch supervised start, server=%+v err=%v", storedGenerated, err)
|
||||
}
|
||||
jobs, err = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: generatedRunDraft.ID})
|
||||
if err != nil || len(jobs) != 1 || jobs[0].Capability != domain.LifecycleCapabilityStart || jobs[0].ExecutionInput.WorkspaceScope != "local" {
|
||||
t.Fatalf("expected one scoped automatic generated Run start job, jobs=%+v err=%v", jobs, err)
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("expected generated Run startup to be autonomous, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
|
||||
existing, err := svc.CreateServerInstanceWorkflowForSession(owner, domain.ServerLifecycleCreate{ID: "managed-existing", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Managed Existing", IdempotencyKey: "managed-existing-create", ProfileKey: "local", Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeExisting, ServerRoot: "C:\\existing-scum"}})
|
||||
@@ -337,7 +337,7 @@ func TestCoreServiceDedicatedRunRegistrationAutomaticallyDeploysGuidedDraftOnly(
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceGeneratedSCUMRunRegistrationQueuesGuidedStart(t *testing.T) {
|
||||
func TestCoreServiceGeneratedSCUMRunRegistrationDoesNotQueueGuidedStart(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin := scumDeploymentTestPlugin()
|
||||
plugin.Name = "SCUM"
|
||||
@@ -423,52 +423,34 @@ func TestCoreServiceGeneratedSCUMRunRegistrationQueuesGuidedStart(t *testing.T)
|
||||
}
|
||||
|
||||
stored, err := svc.GetServerInstance(instance.ID)
|
||||
if err != nil || stored.State != domain.ServerInstanceStateInstalling {
|
||||
t.Fatalf("generated SCUM registration should queue supervised bootstrap start, server=%+v err=%v", stored, err)
|
||||
if err != nil || stored.State != domain.ServerInstanceStateDraft {
|
||||
t.Fatalf("generated SCUM registration must leave startup to Run, server=%+v err=%v", stored, err)
|
||||
}
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(jobs) != 1 {
|
||||
t.Fatalf("expected one SCUM start job, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
job := jobs[0]
|
||||
if job.Capability != domain.LifecycleCapabilityStart || job.TargetKey != "actions/start.json" || job.ExecutionInput.WorkspaceScope != "run-local" || job.ExecutionInput.Deployment == nil || job.ExecutionInput.ServerDeploymentPlan != nil || len(job.ExecutionInput.LogSources) == 0 {
|
||||
t.Fatalf("expected SCUM supervised start job with scoped plugin action and generic deployment inputs, job=%+v", job)
|
||||
}
|
||||
if job.ExecutionInput.Deployment.CreateInputs["gamePort"] != "27000" || job.ExecutionInput.Deployment.CreateInputs["maxPlayers"] != "128" {
|
||||
t.Fatalf("SCUM start job lost create inputs: %+v", job.ExecutionInput.Deployment.CreateInputs)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, Capabilities: hello.CapabilityReport.Capabilities, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job.TargetKey != "actions/start.json" || claim.Job.ExecutionInput.WorkspaceScope != "run-local" || claim.Job.ExecutionInput.ServerDeploymentPlan != nil {
|
||||
t.Fatalf("generated SCUM Run should claim scoped plugin-owned start action, claim=%+v err=%v", claim, err)
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("generated SCUM registration must not enqueue start/status jobs, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceGeneratedRunRegistrationReconcilesStaleRunningState(t *testing.T) {
|
||||
func TestCoreServiceGeneratedRunRegistrationDoesNotDispatchStatusReconciliation(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin := createGeneratedRunStatusPlugin(t, svc)
|
||||
instance := domain.ServerInstance{ID: "managed-stale-running", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-stale-running"), Name: "Managed Stale Running", State: domain.ServerInstanceStateRunning, ConfigVersion: 1, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ProfileKey: "run-local", ServerRoot: `D:\scum-stale`, Revision: 1}}
|
||||
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
||||
t.Fatalf("create stale running server: %v", err)
|
||||
}
|
||||
registered := registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
|
||||
registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(jobs) != 1 || jobs[0].Capability != domain.LifecycleCapabilityStatus || jobs[0].TargetKey != "actions/status.json" {
|
||||
t.Fatalf("expected one status reconciliation job, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, Capabilities: []string{domain.LifecycleCapabilityStatus}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != jobs[0].ID {
|
||||
t.Fatalf("claim status reconciliation: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "process status queried"}, Message: "process status queried", ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "not-started", ExitClassification: "not-started", AuditSummary: "bounded process state"}}); err != nil {
|
||||
t.Fatalf("complete status reconciliation: %v", err)
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("registration must not enqueue status reconciliation, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
stored, err := svc.GetServerInstance(instance.ID)
|
||||
if err != nil || stored.State != domain.ServerInstanceStateStopped {
|
||||
t.Fatalf("Run-reported not-started should correct stale running state, server=%+v err=%v", stored, err)
|
||||
if err != nil || stored.State != domain.ServerInstanceStateRunning {
|
||||
t.Fatalf("registration must not mutate projected state without Run report, server=%+v err=%v", stored, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceGeneratedRunRegistrationSkipsStatusWhenLifecycleJobActive(t *testing.T) {
|
||||
func TestCoreServiceGeneratedRunRegistrationLeavesExistingLifecycleJobUntouched(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin := createGeneratedRunStatusPlugin(t, svc)
|
||||
instance := domain.ServerInstance{ID: "managed-active-start", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-active-start"), Name: "Managed Active Start", State: domain.ServerInstanceStateRunning, ConfigVersion: 1, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ProfileKey: "run-local", ServerRoot: `D:\scum-active`, Revision: 1}}
|
||||
@@ -484,7 +466,7 @@ func TestCoreServiceGeneratedRunRegistrationSkipsStatusWhenLifecycleJobActive(t
|
||||
registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(jobs) != 1 || jobs[0].Capability != domain.LifecycleCapabilityStart {
|
||||
t.Fatalf("active lifecycle job should suppress status reconciliation, jobs=%+v err=%v", jobs, err)
|
||||
t.Fatalf("registration should leave pre-existing lifecycle jobs untouched, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,28 +124,29 @@ func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.D
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
profileKey, workspaceSeed, err := svc.runDistributionWorkspaceSeed(distribution)
|
||||
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: 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: workspaceSeed,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -191,26 +192,160 @@ func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.D
|
||||
return domain.DistributionBuildInput{}, repo.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *CoreService) runDistributionWorkspaceSeed(distribution domain.RunDistribution) (string, string, error) {
|
||||
type runDistributionPackageContext struct {
|
||||
profileKey string
|
||||
workspaceSeed string
|
||||
autonomousLifecycle *domain.RunAutonomousLifecyclePlan
|
||||
}
|
||||
|
||||
func (svc *CoreService) runDistributionPackageContext(distribution domain.RunDistribution) (runDistributionPackageContext, error) {
|
||||
instance, err := svc.store.ServerInstances().Get(distribution.ServerInstanceID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return runDistributionPackageContext{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(distribution.PluginID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
seed, err := encodePluginWorkspaceSeed(plugin.LifecycleAssets)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return runDistributionPackageContext{}, err
|
||||
}
|
||||
profileKey := instance.Deployment.ProfileKey
|
||||
bindings := map[string]string(nil)
|
||||
if binding, bindingErr := svc.runtimeBindingForServer(distribution.ServerInstanceID); bindingErr == nil {
|
||||
profileKey = binding.ProfileKey
|
||||
bindings = domain.CopyStringMap(binding.Bindings)
|
||||
} else if !errors.Is(bindingErr, repo.ErrNotFound) {
|
||||
return "", "", bindingErr
|
||||
return runDistributionPackageContext{}, bindingErr
|
||||
}
|
||||
return profileKey, seed, nil
|
||||
plan, err := runAutonomousLifecyclePlan(distribution, instance, plugin, profileKey, bindings)
|
||||
if err != nil {
|
||||
return runDistributionPackageContext{}, err
|
||||
}
|
||||
seed, err := encodeRunWorkspaceSeed(plugin.LifecycleAssets, plan)
|
||||
if err != nil {
|
||||
return runDistributionPackageContext{}, err
|
||||
}
|
||||
return runDistributionPackageContext{profileKey: profileKey, workspaceSeed: seed, autonomousLifecycle: plan}, nil
|
||||
}
|
||||
|
||||
func runAutonomousLifecyclePlan(distribution domain.RunDistribution, instance domain.ServerInstance, plugin domain.GamePlugin, profileKey string, bindings map[string]string) (*domain.RunAutonomousLifecyclePlan, error) {
|
||||
plan := &domain.RunAutonomousLifecyclePlan{
|
||||
SchemaVersion: "1",
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
PluginVersion: plugin.Version,
|
||||
RunEndpointID: distribution.RunEndpointID,
|
||||
ProfileKey: profileKey,
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
TargetRelease: distribution.ID,
|
||||
DeploymentRevision: instance.Deployment.Revision,
|
||||
RuntimeBindings: domain.CopyStringMap(bindings),
|
||||
Deployment: autonomousDeploymentFromDefinition(instance.Deployment, profileKey, bindings),
|
||||
}
|
||||
profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, profileKey)
|
||||
for _, action := range []domain.ServerLifecycleAction{domain.ServerLifecycleActionCreate, domain.ServerLifecycleActionStart, domain.ServerLifecycleActionStop, domain.ServerLifecycleActionStatus} {
|
||||
entry := autonomousLifecycleAction(plugin, profile, hasProfile, action)
|
||||
if entry.TargetKey == "" {
|
||||
continue
|
||||
}
|
||||
plan.Actions = append(plan.Actions, entry)
|
||||
}
|
||||
bootstrap := autonomousLifecycleAction(plugin, profile, hasProfile, autonomousBootstrapLifecycleAction(plugin, profile, hasProfile))
|
||||
if bootstrap.TargetKey != "" {
|
||||
plan.Bootstrap = &bootstrap
|
||||
}
|
||||
for _, probe := range plugin.RuntimeProfiles.DependencyProbes {
|
||||
if runtimePlatformsContain(probe.Platforms, distribution.TargetOS) {
|
||||
plan.DependencyProbes = append(plan.DependencyProbes, autonomousDependencyProbe(probe))
|
||||
}
|
||||
}
|
||||
for _, installPlan := range plugin.RuntimeProfiles.InstallPlans {
|
||||
if runtimePlatformsContain(installPlan.Platforms, distribution.TargetOS) {
|
||||
plan.InstallPlans = append(plan.InstallPlans, autonomousInstallPlan(installPlan))
|
||||
}
|
||||
}
|
||||
for _, source := range plugin.RuntimeProfiles.LogSources {
|
||||
if source.Kind == "process.stdout" || source.Kind == "process.stderr" {
|
||||
plan.LogSources = append(plan.LogSources, autonomousLogSource(source))
|
||||
}
|
||||
}
|
||||
if hasProfile && len(profile.DLLExtensionRefs) > 0 {
|
||||
endpoint := domain.RunEndpoint{ID: distribution.RunEndpointID, Platform: distribution.TargetOS, Architecture: distribution.TargetArch}
|
||||
extensions, err := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, extension := range extensions {
|
||||
plan.DLLExtensions = append(plan.DLLExtensions, autonomousDLLExtension(extension))
|
||||
}
|
||||
}
|
||||
return domain.CopyRunAutonomousLifecyclePlanPtr(plan), nil
|
||||
}
|
||||
|
||||
func autonomousLifecycleAction(plugin domain.GamePlugin, profile domain.RuntimeLifecycleProfile, hasProfile bool, action domain.ServerLifecycleAction) domain.RunAutonomousLifecycleAction {
|
||||
targetKey := ""
|
||||
if hasProfile {
|
||||
targetKey = runtimeProfileActionRef(profile.ActionRefs, action)
|
||||
}
|
||||
if targetKey == "" {
|
||||
targetKey = lifecycleActionRef(plugin, action)
|
||||
}
|
||||
if targetKey == "" {
|
||||
return domain.RunAutonomousLifecycleAction{}
|
||||
}
|
||||
return domain.RunAutonomousLifecycleAction{Action: action, Operation: lifecycleExecutionOperation(action), Capability: domain.LifecycleCapabilityForAction(action), TargetKey: targetKey}
|
||||
}
|
||||
|
||||
func autonomousBootstrapLifecycleAction(plugin domain.GamePlugin, profile domain.RuntimeLifecycleProfile, hasProfile bool) domain.ServerLifecycleAction {
|
||||
if autonomousLifecycleAction(plugin, profile, hasProfile, domain.ServerLifecycleActionStart).TargetKey != "" {
|
||||
return domain.ServerLifecycleActionStart
|
||||
}
|
||||
return domain.ServerLifecycleActionCreate
|
||||
}
|
||||
|
||||
func autonomousDependencyProbe(probe domain.RuntimeDependencyProbe) domain.RunAutonomousDependencyProbe {
|
||||
return domain.RunAutonomousDependencyProbe{Key: probe.Key, Kind: probe.Kind, TargetKey: probe.TargetKey, Required: probe.Required, MinimumVersion: probe.MinimumVersion, Platforms: domain.CopyStringSlice(probe.Platforms)}
|
||||
}
|
||||
|
||||
func autonomousInstallPlan(plan domain.RuntimeInstallPlan) domain.RunAutonomousInstallPlan {
|
||||
steps := make([]domain.RunAutonomousInstallStep, len(plan.Steps))
|
||||
for i, step := range plan.Steps {
|
||||
steps[i] = domain.RunAutonomousInstallStep{Type: step.Type, TargetKey: step.TargetKey, PackageManager: step.PackageManager, PackageName: step.PackageName, Version: step.Version, DownloadRef: step.DownloadRef, Checksum: step.Checksum}
|
||||
}
|
||||
return domain.RunAutonomousInstallPlan{Key: plan.Key, Title: plan.Title, Platforms: domain.CopyStringSlice(plan.Platforms), Steps: steps}
|
||||
}
|
||||
|
||||
func autonomousLogSource(source domain.RuntimeLogSource) domain.RunAutonomousLogSource {
|
||||
return domain.RunAutonomousLogSource{Key: source.Key, Kind: source.Kind, TargetKey: source.TargetKey, StreamKey: source.StreamKey, CursorKind: source.CursorKind, RetentionDays: source.RetentionDays}
|
||||
}
|
||||
|
||||
func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.RunAutonomousDLLExtension {
|
||||
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||
}
|
||||
|
||||
func autonomousDeploymentFromDefinition(definition domain.ServerDeploymentDefinition, profileKey string, bindings map[string]string) *domain.RunAutonomousDeployment {
|
||||
if definition.Mode == "" {
|
||||
return nil
|
||||
}
|
||||
copy := domain.CopyServerDeploymentDefinition(definition)
|
||||
if copy.ProfileKey == "" {
|
||||
copy.ProfileKey = profileKey
|
||||
}
|
||||
if len(copy.RuntimeBindings) == 0 {
|
||||
copy.RuntimeBindings = domain.CopyStringMap(bindings)
|
||||
}
|
||||
return &domain.RunAutonomousDeployment{SchemaVersion: "1", Mode: copy.Mode, ProfileKey: copy.ProfileKey, RuntimeBindings: copy.RuntimeBindings, CreateInputs: copy.CreateInputs, ServerRoot: copy.ServerRoot, WorkingDirectory: copy.WorkingDirectory, InstallCommand: copy.InstallCommand, StartCommand: copy.StartCommand, StopCommand: copy.StopCommand, StatusCommand: copy.StatusCommand, Shell: copy.Shell, Revision: copy.Revision}
|
||||
}
|
||||
|
||||
func encodeRunWorkspaceSeed(files []domain.PluginAssetFile, plan *domain.RunAutonomousLifecyclePlan) (string, error) {
|
||||
seedFiles := append([]domain.PluginAssetFile(nil), files...)
|
||||
if plan != nil {
|
||||
payload, err := json.Marshal(plan)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
seedFiles = append(seedFiles, domain.PluginAssetFile{Path: ".platform/autonomous-lifecycle-plan.json", Content: string(payload), Mode: 0o600})
|
||||
}
|
||||
return encodePluginWorkspaceSeed(seedFiles)
|
||||
}
|
||||
|
||||
func encodePluginWorkspaceSeed(files []domain.PluginAssetFile) (string, error) {
|
||||
|
||||
@@ -41,6 +41,7 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
{Path: "actions/install.json", Content: `{"version":1,"action":"install","mode":"oneshot"}`, Mode: 0o600},
|
||||
{Path: "bin/install-server", Content: "#!/usr/bin/env sh\n", Mode: 0o700},
|
||||
}
|
||||
plugin.RuntimeProfiles.LogSources = append(plugin.RuntimeProfiles.LogSources, domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14})
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("seed plugin lifecycle assets: %v", err)
|
||||
}
|
||||
@@ -52,7 +53,7 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "windows",
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "platform-secret-boundary",
|
||||
})
|
||||
@@ -76,9 +77,26 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
if err := json.Unmarshal(decodedSeed, &seedFiles); err != nil {
|
||||
t.Fatalf("unmarshal workspace seed: %v", err)
|
||||
}
|
||||
if platformInput.ProfileKey != "local" || len(seedFiles) != 2 || seedFiles[1].Path != "bin/install-server" || seedFiles[1].Content == "" {
|
||||
if platformInput.ProfileKey != "local" || len(seedFiles) != 3 || seedFiles[1].Path != "bin/install-server" || seedFiles[1].Content == "" {
|
||||
t.Fatalf("platform builder received incomplete plugin workspace seed: profile=%q seed=%+v", platformInput.ProfileKey, seedFiles)
|
||||
}
|
||||
if seedFiles[2].Path != ".platform/autonomous-lifecycle-plan.json" || seedFiles[2].Content == "" || seedFiles[2].Mode != 0o600 {
|
||||
t.Fatalf("workspace seed did not include autonomous lifecycle plan file: %+v", seedFiles)
|
||||
}
|
||||
plan := platformInput.AutonomousLifecycle
|
||||
if plan == nil || plan.SchemaVersion != "1" || plan.ServerInstanceID != instance.ID || plan.PluginID != plugin.ID || plan.ProfileKey != "local" || plan.Bootstrap == nil || plan.Bootstrap.Action != domain.ServerLifecycleActionStart || plan.Bootstrap.TargetKey != "actions/start.json" {
|
||||
t.Fatalf("platform builder received incomplete autonomous lifecycle plan: %+v", plan)
|
||||
}
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 1 || plan.LogSources[0].Kind != "process.stdout" || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
t.Fatalf("autonomous lifecycle plan lost plugin runtime declarations: %+v", plan)
|
||||
}
|
||||
var seededPlan domain.RunAutonomousLifecyclePlan
|
||||
if err := json.Unmarshal([]byte(seedFiles[2].Content), &seededPlan); err != nil {
|
||||
t.Fatalf("unmarshal seeded autonomous lifecycle plan: %v", err)
|
||||
}
|
||||
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey {
|
||||
t.Fatalf("seeded lifecycle plan differs from build input: seed=%+v input=%+v", seededPlan, plan)
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
|
||||
@@ -51,28 +51,29 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
profileKey, workspaceSeed, err := svc.runDistributionWorkspaceSeed(distribution)
|
||||
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: 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: workspaceSeed,
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -170,6 +171,17 @@ func (builder *DockerDistributionBuilder) Build(input domain.DistributionBuildIn
|
||||
if err := os.WriteFile(filepath.Join(inputDir, "workspace-seed.json"), seedPayload, 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lifecyclePlanPayload := []byte("{}")
|
||||
if input.AutonomousLifecycle != nil {
|
||||
encoded, err := json.Marshal(input.AutonomousLifecycle)
|
||||
if err != nil {
|
||||
return nil, validationError("distribution build input has an invalid autonomous lifecycle plan")
|
||||
}
|
||||
lifecyclePlanPayload = encoded
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(inputDir, "autonomous-lifecycle-plan.json"), lifecyclePlanPayload, 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(inputDir, "build.sh"), []byte(distributionBuildScript), 0o500); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -136,6 +136,13 @@ func TestDockerDistributionBuilderKeepsSecretInIsolatedInput(t *testing.T) {
|
||||
if bytes.Contains(script, []byte(secret)) {
|
||||
t.Fatal("build script must not embed the component auth key")
|
||||
}
|
||||
planPayload, err := os.ReadFile(filepath.Join(inputDir, "autonomous-lifecycle-plan.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read autonomous lifecycle plan input: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(string(planPayload)) != "{}" {
|
||||
t.Fatalf("unexpected empty autonomous lifecycle plan payload %q", planPayload)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(outputDir, "run.exe"), []byte("compiled-run"), 0o700); err != nil {
|
||||
t.Fatalf("write fake build output: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user