feat: auto-deploy guided servers on run registration
This commit is contained in:
@@ -110,6 +110,9 @@ 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
|
||||
}
|
||||
featureFlags := []string{"control.hello", "control.heartbeat", "signed-envelope.v1.optional"}
|
||||
if session.RequireSignedRequests {
|
||||
featureFlags[2] = "signed-envelope.v1.required"
|
||||
@@ -125,6 +128,28 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
}), nil
|
||||
}
|
||||
|
||||
// queueManagedGuidedDeploymentAfterRegistration advances only a newly-created,
|
||||
// target-bound guided server. Selecting guided-install is the owner's prior
|
||||
// authorization for this bounded write; 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 strings.TrimSpace(instance.DeploymentTargetID) == "" || instance.RunEndpointID != hello.RunEndpointID || 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,6 +268,71 @@ func TestCoreServiceComponentRunCannotClaimDistributionBuild(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceDedicatedRunRegistrationAutomaticallyDeploysGuidedDraftOnly(t *testing.T) {
|
||||
svc, _ := newLifecycleRunService(t)
|
||||
plugin := createLifecyclePlugin(t, svc)
|
||||
endpoint, err := svc.store.RunEndpoints().Get("run-local")
|
||||
if err != nil {
|
||||
t.Fatalf("get bootstrap endpoint: %v", err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDistributionBuild, domain.JobCapabilityDeploymentPlan)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("enable bootstrap capabilities: %v", err)
|
||||
}
|
||||
owner := createServiceUserAndLogin(t, svc, domain.User{ID: "managed-deploy-owner", DisplayName: "Managed Deploy Owner", Email: "managed-deploy@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
|
||||
guided, err := svc.CreateServerInstanceWorkflowForSession(owner, domain.ServerLifecycleCreate{ID: "managed-guided", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Managed Guided", IdempotencyKey: "managed-guided-create", ProfileKey: "local", Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: "C:\\scumserver"}})
|
||||
if err != nil {
|
||||
t.Fatalf("create guided draft: %v", err)
|
||||
}
|
||||
if guided.Instance.State != domain.ServerInstanceStateDraft {
|
||||
t.Fatalf("expected draft before Run registration, got %+v", guided.Instance)
|
||||
}
|
||||
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 install, 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.LifecycleCapabilityInstall {
|
||||
t.Fatalf("expected one automatic install job, 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 install, jobs=%+v", jobs)
|
||||
}
|
||||
|
||||
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"}})
|
||||
if err != nil {
|
||||
t.Fatalf("create existing draft: %v", err)
|
||||
}
|
||||
registerDedicatedRunForTest(t, svc, existing.Instance, plugin.ID)
|
||||
jobs, err = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: existing.Instance.ID})
|
||||
if err != nil || len(jobs) != 0 {
|
||||
t.Fatalf("existing-server registration must not reinstall, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
}
|
||||
|
||||
func registerDedicatedRunForTest(t *testing.T, svc *CoreService, instance domain.ServerInstance, pluginID string) {
|
||||
t.Helper()
|
||||
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
||||
if err != nil {
|
||||
t.Fatalf("get dedicated Run key: %v", err)
|
||||
}
|
||||
hello := validRunControlHello()
|
||||
hello.RunEndpointID = instance.RunEndpointID
|
||||
hello.RegistrationToken = plainKey
|
||||
hello.ServerInstanceID = instance.ID
|
||||
hello.PluginID = pluginID
|
||||
hello.ComponentKind = domain.DistributionComponentRun
|
||||
hello.KeyGeneration = key.Generation
|
||||
hello.CapabilityReport.Capabilities = append(hello.CapabilityReport.Capabilities, domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.JobCapabilityDeploymentPlan, "logs.read")
|
||||
if result, err := svc.RegisterRunHello(hello); err != nil || !result.Accepted {
|
||||
t.Fatalf("register dedicated Run: result=%+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRequestsCapabilityRefreshOnFingerprintDrift(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
hello, err := svc.RegisterRunHello(validRunControlHello())
|
||||
|
||||
@@ -91,10 +91,17 @@ func (svc *CoreService) UpdateServerDeploymentForSession(sessionID, serverInstan
|
||||
}
|
||||
|
||||
func (svc *CoreService) DeployServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
||||
if err := validator.ValidateServerLifecycleCommand(command); err != nil {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil {
|
||||
return svc.deployServerInstance(command)
|
||||
}
|
||||
|
||||
// deployServerInstance is the platform-owned transition from a saved deployment
|
||||
// definition to one fenced install job. Callers must already have established
|
||||
// the authority to act for the server.
|
||||
func (svc *CoreService) deployServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) {
|
||||
if err := validator.ValidateServerLifecycleCommand(command); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(command.ServerInstanceID)
|
||||
|
||||
Reference in New Issue
Block a user