feat: 清理openspec
This commit is contained in:
@@ -640,16 +640,10 @@ func TestServerLifecycleWorkflowAPI(t *testing.T) {
|
||||
if created.Action != domain.ServerLifecycleActionCreate || created.Instance.State != domain.ServerInstanceStateDraft || created.Job.ID != "" || created.Instance.RunEndpointID != "" {
|
||||
t.Fatalf("expected create workflow response, got %+v", created)
|
||||
}
|
||||
legacyCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{
|
||||
ID: "server-create-legacy",
|
||||
PluginID: "server.scum",
|
||||
RunEndpointID: "run-local",
|
||||
Name: "SCUM Legacy Create",
|
||||
IdempotencyKey: "idem-create-legacy",
|
||||
ProfileKey: "local",
|
||||
Bindings: map[string]string{"server-root": "runtime.server-root"},
|
||||
legacyCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", map[string]any{
|
||||
"id": "server-create-legacy", "pluginId": "server.scum", "runEndpointId": "run-local", "name": "SCUM Legacy Create", "idempotencyKey": "idem-create-legacy", "profileKey": "local", "bindings": map[string]string{"server-root": "runtime.server-root"},
|
||||
}, adminSession)
|
||||
assertErrorResponse(t, legacyCreate, http.StatusBadRequest, errorCodeValidation)
|
||||
assertErrorResponse(t, legacyCreate, http.StatusBadRequest, errorCodeBadRequest)
|
||||
|
||||
ready := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||
ID: "server-ready",
|
||||
@@ -1492,10 +1486,10 @@ func TestRuntimeBindingAPIIsAuthorizedValidatedAndRedacted(t *testing.T) {
|
||||
undeclared := requestJSONWithAuth(t, router, http.MethodPut, "/api/v1/server-instances/"+server.ID+"/runtime-binding", dto.RuntimeBindingUpdateRequest{ProfileKey: "local", Bindings: map[string]string{"host.socket": "runtime.socket"}}, adminSession)
|
||||
assertErrorResponse(t, undeclared, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
legacyCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "runtime-create-complete", PluginID: registration.Manifest.ID, RunEndpointID: "run-local", Name: "Runtime Create Complete", IdempotencyKey: "runtime-create-complete", ProfileKey: "local", Bindings: map[string]string{"server-root": "runtime.server-root", "rcon.password": "secret://runtime-create-complete/rcon"}}, adminSession)
|
||||
assertErrorResponse(t, legacyCreate, http.StatusBadRequest, errorCodeValidation)
|
||||
incompleteCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "runtime-create-incomplete", PluginID: registration.Manifest.ID, RunEndpointID: "run-local", Name: "Runtime Create Incomplete", IdempotencyKey: "runtime-create-incomplete", ProfileKey: "local", Bindings: map[string]string{"server-root": "runtime.server-root"}}, adminSession)
|
||||
assertErrorResponse(t, incompleteCreate, http.StatusBadRequest, errorCodeValidation)
|
||||
legacyCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", map[string]any{"id": "runtime-create-complete", "pluginId": registration.Manifest.ID, "runEndpointId": "run-local", "name": "Runtime Create Complete", "idempotencyKey": "runtime-create-complete", "profileKey": "local", "bindings": map[string]string{"server-root": "runtime.server-root", "rcon.password": "secret://runtime-create-complete/rcon"}}, adminSession)
|
||||
assertErrorResponse(t, legacyCreate, http.StatusBadRequest, errorCodeBadRequest)
|
||||
incompleteCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", map[string]any{"id": "runtime-create-incomplete", "pluginId": registration.Manifest.ID, "runEndpointId": "run-local", "name": "Runtime Create Incomplete", "idempotencyKey": "runtime-create-incomplete", "profileKey": "local", "bindings": map[string]string{"server-root": "runtime.server-root"}}, adminSession)
|
||||
assertErrorResponse(t, incompleteCreate, http.StatusBadRequest, errorCodeBadRequest)
|
||||
missingServer := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/runtime-create-incomplete", "", adminSession)
|
||||
assertErrorResponse(t, missingServer, http.StatusNotFound, errorCodeNotFound)
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ Artifact bridge execution returns safe metadata and platform content routes only
|
||||
|
||||
## Implemented Server Lifecycle Actions
|
||||
|
||||
- `POST /api/v1/server-instances/workflows/create`: accepts `ServerLifecycleCreateRequest`. Creation starts from `pluginId` and `name`, and may include the create-wizard deployment definition such as deployment mode, plugin create inputs, server root, or custom start command. The browser must not require or submit a deployment target, run endpoint, runtime profile, or runtime bindings during creation; those bindings remain post-creation/runtime-registration concerns. Requests that include `deploymentTargetId`, `runEndpointId`, `profileKey`, `bindings`, `deployment.runEndpointId`, `deployment.profileKey`, or `deployment.runtimeBindings` are rejected.
|
||||
- `POST /api/v1/server-instances/workflows/create`: accepts `ServerLifecycleCreateRequest`. Creation starts from `pluginId` and `name`, and may include the create-wizard deployment definition such as deployment mode, plugin create inputs, server root, or custom start command. The browser does not submit a deployment target, Run endpoint, lifecycle profile, or Run identity binding. Platform applies plugin defaults, creates the definition without waiting for a Run, and attaches the active Run when its authenticated heartbeat arrives. Unknown legacy binding fields are rejected by the strict JSON decoder.
|
||||
- `POST /api/v1/server-instances/{id}/start`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.start` job using `ServerLifecycleResponse`.
|
||||
- `POST /api/v1/server-instances/{id}/stop`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.stop` job using `ServerLifecycleResponse`.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// serverInstanceCreateWorkflow godoc
|
||||
// @Summary Create server instance workflow
|
||||
// @Description Creates a server definition. A deployment target saves a draft and reserves a dedicated Run identity; installation is queued only after that Run registers and deployment is requested.
|
||||
// @Description Creates a server definition. Run is discovered automatically from its heartbeat; operators do not select a node or lifecycle profile.
|
||||
// @Tags server-instances
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -29,10 +29,6 @@ func (h *coreHandlers) serverInstanceCreateWorkflow(w http.ResponseWriter, r *ht
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
if err := request.ValidateCreateOnly(); err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
result, err := h.core.CreateServerInstanceWorkflowForSession(bearerToken(r), request.ToDomain())
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
@@ -83,7 +79,7 @@ func (h *coreHandlers) serverDeployment(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// serverInstanceDeploy binds an existing draft definition to its configured Run and queues install.
|
||||
// serverInstanceDeploy is retained for the internal lifecycle compatibility route; new Run packages execute their autonomous plan and report heartbeat/activity without an operator binding step.
|
||||
func (h *coreHandlers) serverInstanceDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
|
||||
@@ -771,9 +771,8 @@ type ServerInstance struct {
|
||||
ID string
|
||||
PluginID string
|
||||
PluginVersion string
|
||||
// DeploymentTargetID identifies an optional operator-selected deployment
|
||||
// target for post-creation deployment operations. It never selects a
|
||||
// distribution builder or replaces the server's generated Run endpoint.
|
||||
// DeploymentTargetID is retained only for loading legacy snapshots. New API
|
||||
// flows never write or expose an operator-selected deployment target.
|
||||
DeploymentTargetID string
|
||||
RunEndpointID string
|
||||
Name string
|
||||
|
||||
@@ -354,7 +354,7 @@ type GameClientBridgeDataPackDeclarationBody struct {
|
||||
DatabaseUserVersion int `json:"databaseUserVersion"`
|
||||
LogParserRefs []string `json:"logParserRefs"`
|
||||
ConfigMapRefs []string `json:"configMapRefs"`
|
||||
DataRefs []string `json:"dataRefs,omitempty"`
|
||||
DataRefs []string `json:"dataRefs,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgeOperationSafetyBody struct {
|
||||
@@ -659,7 +659,6 @@ type ServerInstanceResponse struct {
|
||||
ID string `json:"id"`
|
||||
PluginID string `json:"pluginId"`
|
||||
PluginVersion string `json:"pluginVersion"`
|
||||
DeploymentTargetID string `json:"deploymentTargetId,omitempty"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
Name string `json:"name"`
|
||||
OwnerUserID string `json:"ownerUserId,omitempty"`
|
||||
@@ -1245,7 +1244,7 @@ func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManif
|
||||
}
|
||||
dataPacks := make([]domain.GameClientBridgeDataPackDeclaration, len(body.DataPacks))
|
||||
for index, dataPack := range body.DataPacks {
|
||||
dataPacks[index] = domain.GameClientBridgeDataPackDeclaration{Key: dataPack.Key, DatabaseUserVersion: dataPack.DatabaseUserVersion, LogParserRefs: domain.CopyStringSlice(dataPack.LogParserRefs), ConfigMapRefs: domain.CopyStringSlice(dataPack.ConfigMapRefs), DataRefs: domain.CopyStringSlice(dataPack.DataRefs)}
|
||||
dataPacks[index] = domain.GameClientBridgeDataPackDeclaration{Key: dataPack.Key, DatabaseUserVersion: dataPack.DatabaseUserVersion, LogParserRefs: domain.CopyStringSlice(dataPack.LogParserRefs), ConfigMapRefs: domain.CopyStringSlice(dataPack.ConfigMapRefs), DataRefs: domain.CopyStringSlice(dataPack.DataRefs)}
|
||||
}
|
||||
operationTemplates := make([]domain.GameClientBridgeOperationTemplateDeclaration, len(body.OperationTemplates))
|
||||
for index, template := range body.OperationTemplates {
|
||||
@@ -1731,7 +1730,7 @@ func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) G
|
||||
}
|
||||
dataPacks := make([]GameClientBridgeDataPackDeclarationBody, len(value.DataPacks))
|
||||
for index, dataPack := range value.DataPacks {
|
||||
dataPacks[index] = GameClientBridgeDataPackDeclarationBody{Key: dataPack.Key, DatabaseUserVersion: dataPack.DatabaseUserVersion, LogParserRefs: domain.CopyStringSlice(dataPack.LogParserRefs), ConfigMapRefs: domain.CopyStringSlice(dataPack.ConfigMapRefs), DataRefs: domain.CopyStringSlice(dataPack.DataRefs)}
|
||||
dataPacks[index] = GameClientBridgeDataPackDeclarationBody{Key: dataPack.Key, DatabaseUserVersion: dataPack.DatabaseUserVersion, LogParserRefs: domain.CopyStringSlice(dataPack.LogParserRefs), ConfigMapRefs: domain.CopyStringSlice(dataPack.ConfigMapRefs), DataRefs: domain.CopyStringSlice(dataPack.DataRefs)}
|
||||
}
|
||||
operationTemplates := make([]GameClientBridgeOperationTemplateDeclarationBody, len(value.OperationTemplates))
|
||||
for index, template := range value.OperationTemplates {
|
||||
@@ -1820,7 +1819,6 @@ func ServerInstanceFromDomain(instance domain.ServerInstance) ServerInstanceResp
|
||||
ID: instance.ID,
|
||||
PluginID: instance.PluginID,
|
||||
PluginVersion: instance.PluginVersion,
|
||||
DeploymentTargetID: instance.DeploymentTargetID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Name: instance.Name,
|
||||
OwnerUserID: instance.OwnerUserID,
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
type ServerDeploymentRequest struct {
|
||||
RunEndpointID string `json:"runEndpointId,omitempty"`
|
||||
Mode domain.ServerDeploymentMode `json:"mode"`
|
||||
ProfileKey string `json:"profileKey,omitempty"`
|
||||
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
|
||||
CreateInputs map[string]string `json:"createInputs,omitempty"`
|
||||
ServerRoot string `json:"serverRoot,omitempty"`
|
||||
WorkingDirectory string `json:"workingDirectory,omitempty"`
|
||||
@@ -27,7 +22,6 @@ type ServerDeploymentRequest struct {
|
||||
type ServerDeploymentResponse struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
Mode domain.ServerDeploymentMode `json:"mode,omitempty"`
|
||||
ProfileKey string `json:"profileKey,omitempty"`
|
||||
CreateInputs map[string]string `json:"createInputs,omitempty"`
|
||||
ServerRootConfigured bool `json:"serverRootConfigured"`
|
||||
WorkingDirectoryConfigured bool `json:"workingDirectoryConfigured"`
|
||||
@@ -77,45 +71,12 @@ type ServerDeploymentProjectionBody struct {
|
||||
}
|
||||
|
||||
type ServerLifecycleCreateRequest struct {
|
||||
ID string `json:"id"`
|
||||
PluginID string `json:"pluginId"`
|
||||
DeploymentTargetID string `json:"deploymentTargetId,omitempty"`
|
||||
RunEndpointID string `json:"runEndpointId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
OwnerUserID string `json:"ownerUserId,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Bindings map[string]string `json:"bindings,omitempty"`
|
||||
Deployment ServerDeploymentRequest `json:"deployment,omitempty"`
|
||||
}
|
||||
|
||||
func (request ServerLifecycleCreateRequest) ValidateCreateOnly() error {
|
||||
var violations []string
|
||||
if strings.TrimSpace(request.DeploymentTargetID) != "" {
|
||||
violations = append(violations, "deploymentTargetId must not be provided during server creation")
|
||||
}
|
||||
if strings.TrimSpace(request.RunEndpointID) != "" {
|
||||
violations = append(violations, "runEndpointId must not be provided during server creation")
|
||||
}
|
||||
if strings.TrimSpace(request.ProfileKey) != "" {
|
||||
violations = append(violations, "profileKey must not be provided during server creation")
|
||||
}
|
||||
if len(request.Bindings) > 0 {
|
||||
violations = append(violations, "bindings must not be provided during server creation")
|
||||
}
|
||||
if strings.TrimSpace(request.Deployment.RunEndpointID) != "" {
|
||||
violations = append(violations, "deployment.runEndpointId must not be provided during server creation")
|
||||
}
|
||||
if strings.TrimSpace(request.Deployment.ProfileKey) != "" {
|
||||
violations = append(violations, "deployment.profileKey must not be provided during server creation")
|
||||
}
|
||||
if len(request.Deployment.RuntimeBindings) > 0 {
|
||||
violations = append(violations, "deployment.runtimeBindings must not be provided during server creation")
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
return validator.ValidationError{Violations: violations}
|
||||
}
|
||||
return nil
|
||||
ID string `json:"id"`
|
||||
PluginID string `json:"pluginId"`
|
||||
Name string `json:"name"`
|
||||
OwnerUserID string `json:"ownerUserId,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Deployment ServerDeploymentRequest `json:"deployment,omitempty"`
|
||||
}
|
||||
|
||||
type ServerLifecycleCommandRequest struct {
|
||||
@@ -132,21 +93,17 @@ type ServerLifecycleResponse struct {
|
||||
|
||||
func (request ServerLifecycleCreateRequest) ToDomain() domain.ServerLifecycleCreate {
|
||||
return domain.ServerLifecycleCreate{
|
||||
ID: request.ID,
|
||||
PluginID: request.PluginID,
|
||||
DeploymentTargetID: "",
|
||||
RunEndpointID: "",
|
||||
Name: request.Name,
|
||||
OwnerUserID: request.OwnerUserID,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
ProfileKey: "",
|
||||
Bindings: nil,
|
||||
Deployment: request.Deployment.deploymentDefinition(),
|
||||
ID: request.ID,
|
||||
PluginID: request.PluginID,
|
||||
Name: request.Name,
|
||||
OwnerUserID: request.OwnerUserID,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Deployment: request.Deployment.deploymentDefinition(),
|
||||
}
|
||||
}
|
||||
|
||||
func (request ServerDeploymentRequest) ToDomain() domain.ServerDeploymentUpdate {
|
||||
update := domain.ServerDeploymentUpdate{RunEndpointID: request.RunEndpointID, Mode: request.Mode, ProfileKey: request.ProfileKey, RuntimeBindings: domain.CopyStringMap(request.RuntimeBindings), CreateInputs: domain.CopyStringMap(request.CreateInputs), ServerRoot: request.ServerRoot, WorkingDirectory: request.WorkingDirectory, InstallCommand: request.InstallCommand, StartCommand: request.StartCommand, StopCommand: request.StopCommand, StatusCommand: request.StatusCommand, ClearFields: domain.CopyStringSlice(request.ClearFields)}
|
||||
update := domain.ServerDeploymentUpdate{Mode: request.Mode, CreateInputs: domain.CopyStringMap(request.CreateInputs), ServerRoot: request.ServerRoot, WorkingDirectory: request.WorkingDirectory, InstallCommand: request.InstallCommand, StartCommand: request.StartCommand, StopCommand: request.StopCommand, StatusCommand: request.StatusCommand, ClearFields: domain.CopyStringSlice(request.ClearFields)}
|
||||
if request.Shell != nil {
|
||||
update.Shell, update.ShellSet = *request.Shell, true
|
||||
}
|
||||
@@ -155,11 +112,11 @@ func (request ServerDeploymentRequest) ToDomain() domain.ServerDeploymentUpdate
|
||||
|
||||
func (request ServerDeploymentRequest) deploymentDefinition() domain.ServerDeploymentDefinition {
|
||||
update := request.ToDomain()
|
||||
return domain.ServerDeploymentDefinition{Mode: update.Mode, ProfileKey: update.ProfileKey, RuntimeBindings: update.RuntimeBindings, CreateInputs: update.CreateInputs, ServerRoot: update.ServerRoot, WorkingDirectory: update.WorkingDirectory, InstallCommand: update.InstallCommand, StartCommand: update.StartCommand, StopCommand: update.StopCommand, StatusCommand: update.StatusCommand, Shell: update.Shell}
|
||||
return domain.ServerDeploymentDefinition{Mode: update.Mode, CreateInputs: update.CreateInputs, ServerRoot: update.ServerRoot, WorkingDirectory: update.WorkingDirectory, InstallCommand: update.InstallCommand, StartCommand: update.StartCommand, StopCommand: update.StopCommand, StatusCommand: update.StatusCommand, Shell: update.Shell}
|
||||
}
|
||||
|
||||
func ServerDeploymentFromDomain(view domain.ServerDeploymentView) ServerDeploymentResponse {
|
||||
return ServerDeploymentResponse{ServerInstanceID: view.ServerInstanceID, Mode: view.Mode, ProfileKey: view.ProfileKey, CreateInputs: domain.CopyStringMap(view.CreateInputs), ServerRootConfigured: view.ServerRootConfigured, WorkingDirectoryConfigured: view.WorkingDirectoryConfigured, InstallCommandConfigured: view.InstallCommandConfigured, StartCommandConfigured: view.StartCommandConfigured, StopCommandConfigured: view.StopCommandConfigured, StatusCommandConfigured: view.StatusCommandConfigured, Shell: view.Shell, Revision: view.Revision, UpdatedAt: optionalTime(view.UpdatedAt), Projection: deploymentProjectionFromDomain(view.Projection), LatestDispatch: deploymentDispatchEvidenceFromDomain(view.LatestDispatch)}
|
||||
return ServerDeploymentResponse{ServerInstanceID: view.ServerInstanceID, Mode: view.Mode, CreateInputs: domain.CopyStringMap(view.CreateInputs), ServerRootConfigured: view.ServerRootConfigured, WorkingDirectoryConfigured: view.WorkingDirectoryConfigured, InstallCommandConfigured: view.InstallCommandConfigured, StartCommandConfigured: view.StartCommandConfigured, StopCommandConfigured: view.StopCommandConfigured, StatusCommandConfigured: view.StatusCommandConfigured, Shell: view.Shell, Revision: view.Revision, UpdatedAt: optionalTime(view.UpdatedAt), Projection: deploymentProjectionFromDomain(view.Projection), LatestDispatch: deploymentDispatchEvidenceFromDomain(view.LatestDispatch)}
|
||||
}
|
||||
|
||||
func ServerDeploymentRevealFromDomain(reveal domain.ServerDeploymentReveal) ServerDeploymentRevealResponse {
|
||||
|
||||
@@ -204,8 +204,8 @@ type ServerInstance struct {
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
// PluginVersion records the plugin version used for creation or reconcile.
|
||||
PluginVersion string `json:"pluginVersion" db:"plugin_version"`
|
||||
// DeploymentTargetID references an optional post-creation deployment target;
|
||||
// platform-owned distribution builds never use it as a builder selector.
|
||||
// DeploymentTargetID is retained only for legacy snapshots. Current flows
|
||||
// attach Run identity from authenticated heartbeats instead of selecting it.
|
||||
DeploymentTargetID string `json:"deploymentTargetId,omitempty" db:"deployment_target_id"`
|
||||
// RunEndpointID references the dedicated server Run endpoint.
|
||||
RunEndpointID string `json:"runEndpointId" db:"run_endpoint_id"`
|
||||
|
||||
@@ -30,7 +30,7 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
if hasComponentAuthIdentity(hello) {
|
||||
if err := svc.validateDedicatedRunHello(hello); err != nil {
|
||||
if err := svc.validateAuthenticatedRunHello(hello); err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
@@ -76,6 +76,19 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
if err := svc.upsertRunEndpoint(endpoint); err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
if hello.ServerInstanceID != "" && hello.ComponentKind == domain.DistributionComponentRun {
|
||||
instance, instanceErr := svc.store.ServerInstances().Get(hello.ServerInstanceID)
|
||||
if instanceErr != nil {
|
||||
return domain.RunControlHelloResult{}, instanceErr
|
||||
}
|
||||
if instance.RunEndpointID != hello.RunEndpointID {
|
||||
instance.RunEndpointID = hello.RunEndpointID
|
||||
instance.UpdatedAt = stamp
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
previous, previousErr := svc.store.RunControlSessions().Get(hello.RunEndpointID)
|
||||
generation := 1
|
||||
if previousErr == nil {
|
||||
@@ -126,19 +139,22 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) validateDedicatedRunHello(hello domain.RunControlHello) error {
|
||||
func (svc *CoreService) validateAuthenticatedRunHello(hello domain.RunControlHello) error {
|
||||
if hello.ComponentKind != domain.DistributionComponentRun {
|
||||
return validationError("component-authenticated run hello must use the run component")
|
||||
}
|
||||
if hello.RunEndpointID == platformDistributionBuilderEndpointID {
|
||||
return validationError("platform distribution builder cannot register as a server Run")
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(hello.ServerInstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(instance.DeploymentTargetID) == "" && instance.RunEndpointID != dedicatedRunEndpointID(instance.ID) {
|
||||
return nil // legacy Run registrations keep their historical endpoint contract.
|
||||
if hello.PluginID != instance.PluginID {
|
||||
return validationError("Run plugin identity does not match the server instance")
|
||||
}
|
||||
if hello.PluginID != instance.PluginID || hello.RunEndpointID != instance.RunEndpointID {
|
||||
return validationError("run endpoint identity does not match the server binding")
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return validationError("deleted server cannot attach an active Run heartbeat")
|
||||
}
|
||||
instances, err := svc.store.ServerInstances().List(domain.ServerInstanceFilter{RunEndpointID: hello.RunEndpointID})
|
||||
if err != nil {
|
||||
|
||||
@@ -205,7 +205,6 @@ func TestCoreServiceRunHelloRejectsStalePackageKeyAfterReset(t *testing.T) {
|
||||
|
||||
func TestCoreServiceRunHelloRejectsGeneratedRunOnPromotedBuildEndpoint(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
builderID := instance.RunEndpointID
|
||||
instance.State = domain.ServerInstanceStateFailed
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
t.Fatalf("mark legacy server failed: %v", err)
|
||||
@@ -213,28 +212,28 @@ func TestCoreServiceRunHelloRejectsGeneratedRunOnPromotedBuildEndpoint(t *testin
|
||||
if _, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "promoted-hello-fence"}); err != nil {
|
||||
t.Fatalf("generate promoted Run: %v", err)
|
||||
}
|
||||
migrated, err := svc.GetServerInstance(instance.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get migrated server: %v", err)
|
||||
}
|
||||
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
||||
if err != nil {
|
||||
t.Fatalf("get component key: %v", err)
|
||||
}
|
||||
hello := validRunControlHello()
|
||||
hello.RunEndpointID = builderID
|
||||
hello.RunEndpointID = platformDistributionBuilderEndpointID
|
||||
hello.RegistrationToken = plainKey
|
||||
hello.ServerInstanceID = instance.ID
|
||||
hello.PluginID = instance.PluginID
|
||||
hello.ComponentKind = domain.DistributionComponentRun
|
||||
hello.KeyGeneration = key.Generation
|
||||
if _, err := svc.RegisterRunHello(hello); err == nil || !strings.Contains(err.Error(), "does not match") {
|
||||
if _, err := svc.RegisterRunHello(hello); err == nil || !strings.Contains(err.Error(), "platform distribution builder") {
|
||||
t.Fatalf("expected shared builder registration rejection, got %v", err)
|
||||
}
|
||||
|
||||
hello.RunEndpointID = migrated.RunEndpointID
|
||||
hello.RunEndpointID = generatedRunEndpointID(instance.ID)
|
||||
if result, err := svc.RegisterRunHello(hello); err != nil || !result.Accepted {
|
||||
t.Fatalf("expected dedicated Run registration acceptance, result=%+v err=%v", result, err)
|
||||
t.Fatalf("expected automatically discovered Run registration acceptance, result=%+v err=%v", result, err)
|
||||
}
|
||||
attached, err := svc.GetServerInstance(instance.ID)
|
||||
if err != nil || attached.RunEndpointID != hello.RunEndpointID {
|
||||
t.Fatalf("expected heartbeat to attach active Run endpoint, instance=%+v err=%v", attached, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,19 +274,11 @@ func TestCoreServiceGeneratedRunOnlyEndpointCanGenerateAnotherRun(t *testing.T)
|
||||
packageConfig := readGeneratedPackageConfig(t, svc, session, first.ArtifactID)
|
||||
instance, err = svc.GetServerInstance(instance.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get dedicated Run binding: %v", err)
|
||||
}
|
||||
bootstrap, err := svc.store.RunEndpoints().Get(instance.DeploymentTargetID)
|
||||
if err != nil {
|
||||
t.Fatalf("get former bootstrap endpoint: %v", err)
|
||||
}
|
||||
bootstrap.Status = domain.RunEndpointStatusOffline
|
||||
if err := svc.store.RunEndpoints().Update(bootstrap); err != nil {
|
||||
t.Fatalf("take former bootstrap endpoint offline: %v", err)
|
||||
t.Fatalf("get server after Run build: %v", err)
|
||||
}
|
||||
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.RunEndpointID = instance.RunEndpointID
|
||||
helloRequest.RunEndpointID = first.RunEndpointID
|
||||
helloRequest.RegistrationToken = packageConfig.AuthKey
|
||||
helloRequest.ServerInstanceID = instance.ID
|
||||
helloRequest.PluginID = instance.PluginID
|
||||
@@ -304,7 +296,7 @@ func TestCoreServiceGeneratedRunOnlyEndpointCanGenerateAnotherRun(t *testing.T)
|
||||
t.Fatalf("register generated Run: result=%+v err=%v", registered, err)
|
||||
}
|
||||
online, err := svc.store.RunEndpoints().List(domain.RunEndpointFilter{Status: domain.RunEndpointStatusOnline})
|
||||
if err != nil || len(online) != 1 || online[0].ID != instance.RunEndpointID {
|
||||
if err != nil || len(online) != 1 || online[0].ID != first.RunEndpointID {
|
||||
t.Fatalf("expected generated Run to be the only online endpoint: endpoints=%+v err=%v", online, err)
|
||||
}
|
||||
for _, capability := range online[0].Capabilities {
|
||||
@@ -313,7 +305,7 @@ func TestCoreServiceGeneratedRunOnlyEndpointCanGenerateAnotherRun(t *testing.T)
|
||||
}
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
RunEndpointID: first.RunEndpointID,
|
||||
SessionToken: registered.SessionToken,
|
||||
Capabilities: []string{domain.JobCapabilityDistributionBuild},
|
||||
Capacity: domain.RunCapacity{MaxJobs: 1},
|
||||
|
||||
@@ -46,9 +46,6 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
}
|
||||
if err := svc.promoteLegacyRunBinding(&instance); err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
if ready, reason := svc.distributionBuilderReadiness(); !ready {
|
||||
_ = svc.recordAuditEvent(user.ID, "run.generate.denied", "server-instance", instance.ID, domain.AuditResultDenied, reason)
|
||||
return domain.RunDistribution{}, validationError(reason)
|
||||
@@ -72,7 +69,7 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
ID: distributionID,
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
RunEndpointID: runEndpointIDForDistribution(instance),
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
PackageFormat: runPackageFormatForTarget(request.TargetOS),
|
||||
@@ -123,29 +120,6 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
return domain.CopyRunDistribution(distribution), nil
|
||||
}
|
||||
|
||||
// promoteLegacyRunBinding reserves the server-scoped endpoint used by a
|
||||
// generated Run. A legacy shared endpoint remains an optional deployment target
|
||||
// for non-build workflows; distribution builds are always platform-owned.
|
||||
func (svc *CoreService) promoteLegacyRunBinding(instance *domain.ServerInstance) error {
|
||||
if instance == nil || strings.TrimSpace(instance.DeploymentTargetID) != "" || (instance.State != domain.ServerInstanceStateDraft && instance.State != domain.ServerInstanceStateFailed) {
|
||||
return nil
|
||||
}
|
||||
currentEndpointID := strings.TrimSpace(instance.RunEndpointID)
|
||||
dedicatedEndpointID := dedicatedRunEndpointID(instance.ID)
|
||||
if currentEndpointID == dedicatedEndpointID {
|
||||
return nil
|
||||
}
|
||||
if currentEndpointID != "" {
|
||||
instance.DeploymentTargetID = currentEndpointID
|
||||
}
|
||||
instance.RunEndpointID = dedicatedEndpointID
|
||||
instance.UpdatedAt = svc.now()
|
||||
if err := validator.ValidateServerInstance(*instance); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.store.ServerInstances().Update(*instance)
|
||||
}
|
||||
|
||||
func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID string, request domain.ClientManagerBuildRequest) (domain.ClientManagerDistribution, error) {
|
||||
request = domain.CopyClientManagerBuildRequest(request)
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" {
|
||||
@@ -458,9 +432,6 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
||||
}
|
||||
endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
runRegistered := endpointErr == nil
|
||||
if endpointErr != nil && strings.TrimSpace(instance.DeploymentTargetID) != "" {
|
||||
endpoint, endpointErr = svc.store.RunEndpoints().Get(instance.DeploymentTargetID)
|
||||
}
|
||||
if endpointErr != nil && !errors.Is(endpointErr, repo.ErrNotFound) {
|
||||
return domain.ServerRuntimeActions{}, endpointErr
|
||||
}
|
||||
@@ -506,15 +477,15 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
||||
Actions: []domain.ServerRuntimeAction{
|
||||
runtimeAction("generate-run", "Generate run", pluginDeclares(plugin, "server.run.distribution") && builderReady && runPackageInputsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared", fallbackReason(!builderReady, builderReason, runPackageReason))),
|
||||
runtimeAction("download-run", "Download run", hasAvailableRunPackage, "run package has not been generated"),
|
||||
runtimeAction("push-run-update", "Push run update", runRegistered && pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && runPackageInputsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", runPackageReason))),
|
||||
runtimeAction("push-run-update", "Push run update", runRegistered && pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && runPackageInputsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", runPackageReason))),
|
||||
runtimeAction("reset-run-key", "Reset run key", pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared"),
|
||||
runtimeAction("generate-client-manager", "Generate client manager", pluginDeclares(plugin, "server.client-manager.manage") && builderReady && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared", fallbackReason(!builderReady, builderReason, bindingReason))),
|
||||
runtimeAction("download-client-manager", "Download client manager", hasAvailableClientPackage, "client-manager package has not been generated"),
|
||||
runtimeAction("reset-client-manager-key", "Reset client-manager key", pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared"),
|
||||
runtimeAction("dependencies-check", "Check dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck), "run endpoint cannot check dependencies", bindingReason)))),
|
||||
runtimeAction("dependencies-install", "Install dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall), "run endpoint cannot install dependencies", bindingReason)))),
|
||||
runtimeAction("live-logs", "Live logs", runRegistered && pluginSupports(plugin, "logs.read"), fallbackReason(!runRegistered, "dedicated Run has not registered", "plugin does not declare live logs")),
|
||||
runtimeAction("historical-logs", "Historical logs", runRegistered && svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill), "run endpoint cannot backfill logs", bindingReason))),
|
||||
runtimeAction("dependencies-check", "Check dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck) && bindingsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck), "run endpoint cannot check dependencies", bindingReason)))),
|
||||
runtimeAction("dependencies-install", "Install dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall) && bindingsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall), "run endpoint cannot install dependencies", bindingReason)))),
|
||||
runtimeAction("live-logs", "Live logs", runRegistered && pluginSupports(plugin, "logs.read"), fallbackReason(!runRegistered, "Run heartbeat has not been observed", "plugin does not declare live logs")),
|
||||
runtimeAction("historical-logs", "Historical logs", runRegistered && svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill) && bindingsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill), "run endpoint cannot backfill logs", bindingReason))),
|
||||
},
|
||||
}
|
||||
if runRegistered {
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestCoreServiceBuildsSCUMGuidedRunWithoutCompleteRuntimeBinding(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServicePromotesLegacyRunBindingBeforeDistributionBuild(t *testing.T) {
|
||||
func TestCoreServiceDoesNotPrebindLegacyRunBeforeDistributionBuild(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
legacyEndpointID := instance.RunEndpointID
|
||||
instance.State = domain.ServerInstanceStateFailed
|
||||
@@ -165,18 +165,18 @@ func TestCoreServicePromotesLegacyRunBindingBeforeDistributionBuild(t *testing.T
|
||||
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "legacy-promote-build"})
|
||||
if err != nil {
|
||||
t.Fatalf("generate promoted legacy Run: %v", err)
|
||||
t.Fatalf("generate Run without prebinding: %v", err)
|
||||
}
|
||||
migrated, err := svc.GetServerInstance(instance.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get migrated server: %v", err)
|
||||
}
|
||||
if migrated.DeploymentTargetID != legacyEndpointID || migrated.RunEndpointID != "server-run-"+instance.ID {
|
||||
t.Fatalf("expected legacy binding promotion, got %+v", migrated)
|
||||
if migrated.DeploymentTargetID != "" || migrated.RunEndpointID != legacyEndpointID {
|
||||
t.Fatalf("Run generation must not change the server's active endpoint, got %+v", migrated)
|
||||
}
|
||||
job, err := svc.GetJob(distribution.BuildJobID)
|
||||
if err != nil || job.RunEndpointID != platformDistributionBuilderEndpointID || distribution.RunEndpointID != migrated.RunEndpointID {
|
||||
t.Fatalf("expected platform build and dedicated package endpoint %q, job=%+v distribution=%+v err=%v", migrated.RunEndpointID, job, distribution, err)
|
||||
if err != nil || job.RunEndpointID != platformDistributionBuilderEndpointID || distribution.RunEndpointID != legacyEndpointID {
|
||||
t.Fatalf("expected platform build and unchanged package endpoint %q, job=%+v distribution=%+v err=%v", legacyEndpointID, job, distribution, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import "browser.local/platform/domain"
|
||||
|
||||
func applyPluginCreateDefaults(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) domain.ServerDeploymentDefinition {
|
||||
definition = domain.CopyServerDeploymentDefinition(definition)
|
||||
if definition.ProfileKey == "" && len(plugin.RuntimeProfiles.LifecycleProfiles) > 0 {
|
||||
definition.ProfileKey = plugin.RuntimeProfiles.LifecycleProfiles[0].Key
|
||||
}
|
||||
if definition.Mode != domain.ServerDeploymentModeGuided {
|
||||
return definition
|
||||
}
|
||||
|
||||
@@ -60,10 +60,7 @@ func (svc *CoreService) UpdateServerDeploymentForSession(sessionID, serverInstan
|
||||
return domain.ServerDeploymentView{}, err
|
||||
}
|
||||
if update.RunEndpointID != "" {
|
||||
if _, err := svc.store.RunEndpoints().Get(update.RunEndpointID); err != nil {
|
||||
return domain.ServerDeploymentView{}, err
|
||||
}
|
||||
instance.RunEndpointID = update.RunEndpointID
|
||||
return domain.ServerDeploymentView{}, validationError("run endpoint identity is managed by Run heartbeat")
|
||||
}
|
||||
instance.Deployment = definition
|
||||
instance.DeploymentProjection = domain.ServerDeploymentProjection{}
|
||||
@@ -105,12 +102,9 @@ func (svc *CoreService) deployServerInstance(command domain.ServerLifecycleComma
|
||||
return domain.ServerLifecycleResult{}, validationError("deployment definition is required")
|
||||
}
|
||||
if strings.TrimSpace(instance.RunEndpointID) == "" {
|
||||
return domain.ServerLifecycleResult{}, validationError("run endpoint must be selected before deployment")
|
||||
return domain.ServerLifecycleResult{}, validationError("an active Run heartbeat is required for legacy manual deployment dispatch")
|
||||
}
|
||||
if _, err := svc.store.RunEndpoints().Get(instance.RunEndpointID); err != nil {
|
||||
if errors.Is(err, repo.ErrNotFound) && strings.TrimSpace(instance.DeploymentTargetID) != "" {
|
||||
return domain.ServerLifecycleResult{}, validationError("dedicated Run must register before deployment")
|
||||
}
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibleRun(t *testing.T) {
|
||||
func TestCoreServiceSavesDraftDeploymentRedactsReadsAndKeepsRunIdentityHeartbeatManaged(t *testing.T) {
|
||||
svc, _ := newLifecycleRunService(t)
|
||||
createLifecyclePlugin(t, svc)
|
||||
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "deployment-owner", DisplayName: "Deployment Owner", Email: "deployment-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
@@ -37,31 +37,8 @@ func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibl
|
||||
t.Fatalf("expected explicit deployment reveal, reveal=%+v err=%v", revealed, err)
|
||||
}
|
||||
|
||||
if _, err := svc.UpdateServerDeploymentForSession(ownerSession, draft.Instance.ID, domain.ServerDeploymentUpdate{RunEndpointID: "run-local", Mode: domain.ServerDeploymentModeCustom}); err != nil {
|
||||
t.Fatalf("bind draft to run: %v", err)
|
||||
}
|
||||
if _, err := svc.DeployServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: draft.Instance.ID, ExpectedConfigVersion: draft.Instance.ConfigVersion, IdempotencyKey: "deploy-incompatible"}); err == nil || !strings.Contains(err.Error(), "deployment.plan.v1") {
|
||||
t.Fatalf("expected incompatible Run rejection, got %v", err)
|
||||
}
|
||||
|
||||
endpoint, err := svc.store.RunEndpoints().Get("run-local")
|
||||
if err != nil {
|
||||
t.Fatalf("get endpoint: %v", err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("enable deployment capability: %v", err)
|
||||
}
|
||||
deployed, err := svc.DeployServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: draft.Instance.ID, ExpectedConfigVersion: draft.Instance.ConfigVersion, IdempotencyKey: "deploy-compatible"})
|
||||
if err != nil {
|
||||
t.Fatalf("deploy compatible draft: %v", err)
|
||||
}
|
||||
if deployed.Job.ExecutionInput.Deployment == nil || deployed.Job.ExecutionInput.Deployment.StartCommand != "/srv/venv-server/.venv/bin/python server.py" || deployed.Job.Progress.Phase != "queued" {
|
||||
t.Fatalf("Run job must carry protected plan and queued phase: %+v", deployed.Job)
|
||||
}
|
||||
view, err = svc.GetServerDeploymentForSession(ownerSession, draft.Instance.ID)
|
||||
if err != nil || view.LatestDispatch == nil || view.LatestDispatch.JobID != deployed.Job.ID || view.LatestDispatch.DeploymentRevision != deployed.Job.ExecutionInput.Deployment.Revision || !view.LatestDispatch.DeploymentDefinitionIncluded {
|
||||
t.Fatalf("expected safe dispatch evidence, view=%+v err=%v", view, err)
|
||||
if _, err := svc.UpdateServerDeploymentForSession(ownerSession, draft.Instance.ID, domain.ServerDeploymentUpdate{RunEndpointID: "run-local", Mode: domain.ServerDeploymentModeCustom}); err == nil || !strings.Contains(err.Error(), "managed by Run heartbeat") {
|
||||
t.Fatalf("expected Run identity update to be rejected, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -152,10 +152,24 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
||||
}), nil
|
||||
}
|
||||
|
||||
func dedicatedRunEndpointID(serverInstanceID string) string {
|
||||
func generatedRunEndpointID(serverInstanceID string) string {
|
||||
return "server-run-" + serverInstanceID
|
||||
}
|
||||
|
||||
// dedicatedRunEndpointID remains a compatibility helper for legacy fixtures.
|
||||
// Production flow uses generatedRunEndpointID only when building a package and
|
||||
// attaches the active endpoint from the first authenticated Run heartbeat.
|
||||
func dedicatedRunEndpointID(serverInstanceID string) string {
|
||||
return generatedRunEndpointID(serverInstanceID)
|
||||
}
|
||||
|
||||
func runEndpointIDForDistribution(instance domain.ServerInstance) string {
|
||||
if endpointID := strings.TrimSpace(instance.RunEndpointID); endpointID != "" {
|
||||
return endpointID
|
||||
}
|
||||
return generatedRunEndpointID(instance.ID)
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateServerInstanceWorkflowForSession(sessionID string, create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) {
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
|
||||
@@ -159,43 +159,36 @@ func assertLogProcessStateEvent(t *testing.T, subscription LogEventSubscription,
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceCreatesTargetBoundDraftAndRequiresDedicatedRunRegistration(t *testing.T) {
|
||||
func TestCoreServiceCreatesUnboundDraftAndAttachesRunFromHeartbeat(t *testing.T) {
|
||||
svc, _ := newLifecycleRunService(t)
|
||||
plugin := createLifecyclePlugin(t, svc)
|
||||
|
||||
draft, err := svc.CreateServerInstanceWorkflow(domain.ServerLifecycleCreate{
|
||||
ID: "server-dedicated", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Dedicated SCUM", IdempotencyKey: "dedicated-draft", ProfileKey: "local",
|
||||
})
|
||||
draft, err := svc.CreateServerInstanceWorkflow(domain.ServerLifecycleCreate{ID: "server-dedicated", PluginID: plugin.ID, Name: "Dedicated SCUM", IdempotencyKey: "dedicated-draft"})
|
||||
if err != nil {
|
||||
t.Fatalf("create target-bound draft: %v", err)
|
||||
}
|
||||
if draft.Instance.State != domain.ServerInstanceStateDraft || draft.Job.ID != "" || draft.Instance.DeploymentTargetID != "run-local" || draft.Instance.RunEndpointID != "server-run-server-dedicated" {
|
||||
t.Fatalf("expected draft with separate target and reserved Run identity, got %+v", draft)
|
||||
}
|
||||
if _, err := svc.DeployServerInstanceForSession("", domain.ServerLifecycleCommand{ServerInstanceID: draft.Instance.ID, ExpectedConfigVersion: draft.Instance.ConfigVersion, IdempotencyKey: "before-register"}); err == nil {
|
||||
t.Fatal("expected deployment without a registered dedicated Run to fail")
|
||||
if draft.Instance.State != domain.ServerInstanceStateDraft || draft.Job.ID != "" || draft.Instance.RunEndpointID != "" {
|
||||
t.Fatalf("expected draft without a reserved Run identity, got %+v", draft)
|
||||
}
|
||||
|
||||
key, plainKey, err := svc.ensureActiveComponentKey(draft.Instance.ID, domain.DistributionComponentRun, "")
|
||||
if err != nil {
|
||||
t.Fatalf("create Run key: %v", err)
|
||||
}
|
||||
wrong := validRunControlHello()
|
||||
wrong.ServerInstanceID = draft.Instance.ID
|
||||
wrong.PluginID = plugin.ID
|
||||
wrong.ComponentKind = domain.DistributionComponentRun
|
||||
wrong.KeyGeneration = key.Generation
|
||||
wrong.RegistrationToken = plainKey
|
||||
wrong.RunEndpointID = "run-local"
|
||||
if _, err := svc.RegisterRunHello(wrong); err == nil || !strings.Contains(err.Error(), "does not match") {
|
||||
t.Fatalf("expected mismatched endpoint registration rejection, got %v", err)
|
||||
hello := validRunControlHello()
|
||||
hello.ServerInstanceID = draft.Instance.ID
|
||||
hello.PluginID = plugin.ID
|
||||
hello.ComponentKind = domain.DistributionComponentRun
|
||||
hello.KeyGeneration = key.Generation
|
||||
hello.RegistrationToken = plainKey
|
||||
hello.RunEndpointID = "run-local"
|
||||
hello.DisplayName = "Automatic SCUM Run"
|
||||
if registered, err := svc.RegisterRunHello(hello); err != nil || !registered.Accepted {
|
||||
t.Fatalf("register automatic Run heartbeat: result=%+v err=%v", registered, err)
|
||||
}
|
||||
|
||||
correct := wrong
|
||||
correct.RunEndpointID = draft.Instance.RunEndpointID
|
||||
correct.DisplayName = "Dedicated SCUM Run"
|
||||
if registered, err := svc.RegisterRunHello(correct); err != nil || !registered.Accepted {
|
||||
t.Fatalf("register dedicated Run: result=%+v err=%v", registered, err)
|
||||
attached, err := svc.GetServerInstance(draft.Instance.ID)
|
||||
if err != nil || attached.RunEndpointID != "run-local" {
|
||||
t.Fatalf("expected heartbeat to attach Run endpoint, instance=%+v err=%v", attached, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user