fix server creation run binding

This commit is contained in:
npc0-hue
2026-08-04 17:14:28 +08:00
parent 980f1786dd
commit 5fbe8092b9
13 changed files with 80 additions and 53 deletions
+16 -10
View File
@@ -783,14 +783,22 @@ func TestServerLifecycleWorkflowAPI(t *testing.T) {
created := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{
ID: "server-create",
PluginID: "server.scum",
RunEndpointID: "run-local",
Name: "SCUM Create",
IdempotencyKey: "idem-create",
ProfileKey: "local",
}, adminSession)
if created.Action != domain.ServerLifecycleActionCreate || created.Instance.State != domain.ServerInstanceStateInstalling || created.Job.Capability != domain.LifecycleCapabilityInstall {
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"},
}, adminSession)
assertErrorResponse(t, legacyCreate, http.StatusBadRequest, errorCodeValidation)
ready := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
ID: "server-ready",
@@ -844,7 +852,7 @@ func TestServerDeploymentRevealAPIIsExplicitAndOwnerScoped(t *testing.T) {
endpoint := validRunEndpointRequest()
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", endpoint)
created := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "deployment-reveal", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Reveal", IdempotencyKey: "deployment-reveal", ProfileKey: "local", Deployment: dto.ServerDeploymentRequest{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/reveal", WorkingDirectory: "/srv/reveal", StartCommand: "./start-server"}}, adminSession)
postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "deployment-reveal", PluginID: "server.scum", Name: "Reveal", IdempotencyKey: "deployment-reveal", Deployment: dto.ServerDeploymentRequest{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/reveal", WorkingDirectory: "/srv/reveal", StartCommand: "./start-server"}}, adminSession)
redactedRecorder := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/deployment-reveal/deployment", "", adminSession)
assertStatus(t, redactedRecorder, http.StatusOK)
@@ -852,8 +860,8 @@ func TestServerDeploymentRevealAPIIsExplicitAndOwnerScoped(t *testing.T) {
t.Fatalf("normal deployment view leaked protected inputs: %s", body)
}
redacted := decodeBody[dto.ServerDeploymentResponse](t, redactedRecorder)
if redacted.LatestDispatch == nil || !redacted.LatestDispatch.DeploymentDefinitionIncluded || redacted.LatestDispatch.JobID != created.Job.ID || redacted.LatestDispatch.DeploymentRevision != 1 {
t.Fatalf("expected safe deployment dispatch evidence, got %+v", redacted.LatestDispatch)
if redacted.LatestDispatch != nil {
t.Fatalf("create-time deployment settings must not dispatch to Run, got %+v", redacted.LatestDispatch)
}
revealed := getJSONWithAuth[dto.ServerDeploymentRevealResponse](t, router, "/api/v1/server-instances/deployment-reveal/deployment/reveal", adminSession)
@@ -1631,10 +1639,8 @@ 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)
created := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/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)
if created.Job.TargetKey != "actions/install.json" {
t.Fatalf("create workflow did not dispatch selected profile: %+v", created.Job)
}
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)
missingServer := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/runtime-create-incomplete", "", adminSession)
+1 -1
View File
@@ -137,7 +137,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 a deployment target, run endpoint, or runtime profile at creation; those bindings remain post-creation/runtime-registration concerns. Legacy `runEndpointId` and `deploymentTargetId` inputs remain accepted for compatible programmatic flows, but are never prerequisites for creation and do not select a distribution builder.
- `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/{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`.
@@ -29,6 +29,10 @@ 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)
+36 -5
View File
@@ -1,9 +1,11 @@
package dto
import (
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/validator"
)
type ServerDeploymentRequest struct {
@@ -78,7 +80,7 @@ type ServerLifecycleCreateRequest struct {
ID string `json:"id"`
PluginID string `json:"pluginId"`
DeploymentTargetID string `json:"deploymentTargetId,omitempty"`
RunEndpointID string `json:"runEndpointId"`
RunEndpointID string `json:"runEndpointId,omitempty"`
Name string `json:"name"`
OwnerUserID string `json:"ownerUserId,omitempty"`
IdempotencyKey string `json:"idempotencyKey"`
@@ -87,6 +89,35 @@ type ServerLifecycleCreateRequest struct {
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
}
type ServerLifecycleCommandRequest struct {
ExpectedConfigVersion int `json:"expectedConfigVersion"`
IdempotencyKey string `json:"idempotencyKey"`
@@ -103,13 +134,13 @@ func (request ServerLifecycleCreateRequest) ToDomain() domain.ServerLifecycleCre
return domain.ServerLifecycleCreate{
ID: request.ID,
PluginID: request.PluginID,
DeploymentTargetID: request.DeploymentTargetID,
RunEndpointID: request.RunEndpointID,
DeploymentTargetID: "",
RunEndpointID: "",
Name: request.Name,
OwnerUserID: request.OwnerUserID,
IdempotencyKey: request.IdempotencyKey,
ProfileKey: request.ProfileKey,
Bindings: domain.CopyStringMap(request.Bindings),
ProfileKey: "",
Bindings: nil,
Deployment: request.Deployment.deploymentDefinition(),
}
}