separate server run bindings from deployment targets
This commit is contained in:
@@ -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`: accept `ServerLifecycleCreateRequest`, validate plugin/run dependencies and idempotency, create an `installing` server instance, and queue a `process.install` job using `ServerLifecycleResponse`.
|
||||
- `POST /api/v1/server-instances/workflows/create`: accepts `ServerLifecycleCreateRequest`. A legacy `runEndpointId` creates and dispatches through the existing endpoint. A `deploymentTargetId` creates a target-bound draft with a reserved dedicated Run identity; no install job is dispatched until that Run registers and `deploy` is requested.
|
||||
- `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 instance through the platform-mediated lifecycle workflow and queues an install job for the selected run endpoint.
|
||||
// @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.
|
||||
// @Tags server-instances
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
||||
@@ -718,21 +718,24 @@ type PluginBridgeExecuteResponse struct {
|
||||
}
|
||||
|
||||
type ServerInstance struct {
|
||||
ID string
|
||||
PluginID string
|
||||
PluginVersion string
|
||||
RunEndpointID string
|
||||
Name string
|
||||
OwnerUserID string
|
||||
AdminUserIDs []string
|
||||
State ServerInstanceState
|
||||
ConfigVersion int
|
||||
ConfigKey string
|
||||
ConfigContent string
|
||||
ConfigChecksum string
|
||||
ConfigUpdatedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
PluginID string
|
||||
PluginVersion string
|
||||
// DeploymentTargetID identifies the trusted existing worker used only to
|
||||
// build a server's dedicated Run package. It is not the server Run itself.
|
||||
DeploymentTargetID string
|
||||
RunEndpointID string
|
||||
Name string
|
||||
OwnerUserID string
|
||||
AdminUserIDs []string
|
||||
State ServerInstanceState
|
||||
ConfigVersion int
|
||||
ConfigKey string
|
||||
ConfigContent string
|
||||
ConfigChecksum string
|
||||
ConfigUpdatedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
// Deployment stores operator-supplied deployment inputs. Its protected path
|
||||
// and command values are never included in normal platform read projections.
|
||||
Deployment ServerDeploymentDefinition
|
||||
|
||||
@@ -17,15 +17,16 @@ const (
|
||||
)
|
||||
|
||||
type ServerLifecycleCreate struct {
|
||||
ID string
|
||||
PluginID string
|
||||
RunEndpointID string
|
||||
Name string
|
||||
OwnerUserID string
|
||||
IdempotencyKey string
|
||||
ProfileKey string
|
||||
Bindings map[string]string
|
||||
Deployment ServerDeploymentDefinition
|
||||
ID string
|
||||
PluginID string
|
||||
DeploymentTargetID string
|
||||
RunEndpointID string
|
||||
Name string
|
||||
OwnerUserID string
|
||||
IdempotencyKey string
|
||||
ProfileKey string
|
||||
Bindings map[string]string
|
||||
Deployment ServerDeploymentDefinition
|
||||
}
|
||||
|
||||
type ServerLifecycleCommand struct {
|
||||
|
||||
@@ -495,6 +495,7 @@ 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"`
|
||||
@@ -1445,6 +1446,7 @@ 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,
|
||||
|
||||
@@ -55,15 +55,16 @@ type ServerDeploymentProjectionBody struct {
|
||||
}
|
||||
|
||||
type ServerLifecycleCreateRequest struct {
|
||||
ID string `json:"id"`
|
||||
PluginID string `json:"pluginId"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
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"`
|
||||
ID string `json:"id"`
|
||||
PluginID string `json:"pluginId"`
|
||||
DeploymentTargetID string `json:"deploymentTargetId,omitempty"`
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type ServerLifecycleCommandRequest struct {
|
||||
@@ -80,15 +81,16 @@ type ServerLifecycleResponse struct {
|
||||
|
||||
func (request ServerLifecycleCreateRequest) ToDomain() domain.ServerLifecycleCreate {
|
||||
return domain.ServerLifecycleCreate{
|
||||
ID: request.ID,
|
||||
PluginID: request.PluginID,
|
||||
RunEndpointID: request.RunEndpointID,
|
||||
Name: request.Name,
|
||||
OwnerUserID: request.OwnerUserID,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
ProfileKey: request.ProfileKey,
|
||||
Bindings: domain.CopyStringMap(request.Bindings),
|
||||
Deployment: request.Deployment.deploymentDefinition(),
|
||||
ID: request.ID,
|
||||
PluginID: request.PluginID,
|
||||
DeploymentTargetID: request.DeploymentTargetID,
|
||||
RunEndpointID: request.RunEndpointID,
|
||||
Name: request.Name,
|
||||
OwnerUserID: request.OwnerUserID,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
ProfileKey: request.ProfileKey,
|
||||
Bindings: domain.CopyStringMap(request.Bindings),
|
||||
Deployment: request.Deployment.deploymentDefinition(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-31
@@ -202,7 +202,9 @@ 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"`
|
||||
// RunEndpointID references the selected run endpoint.
|
||||
// DeploymentTargetID references the trusted worker used to build a dedicated Run.
|
||||
DeploymentTargetID string `json:"deploymentTargetId,omitempty" db:"deployment_target_id"`
|
||||
// RunEndpointID references the dedicated server Run endpoint.
|
||||
RunEndpointID string `json:"runEndpointId" db:"run_endpoint_id"`
|
||||
// Name is the server display name.
|
||||
Name string `json:"name" db:"name"`
|
||||
@@ -728,41 +730,43 @@ func remoteAccessFromDomain(remote domain.GamePluginRemoteAccess) GamePluginRemo
|
||||
|
||||
func ServerInstanceFromDomain(instance domain.ServerInstance) ServerInstance {
|
||||
return ServerInstance{
|
||||
ID: instance.ID,
|
||||
PluginID: instance.PluginID,
|
||||
PluginVersion: instance.PluginVersion,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Name: instance.Name,
|
||||
OwnerUserID: instance.OwnerUserID,
|
||||
AdminUserIDs: domain.CopyStringSlice(instance.AdminUserIDs),
|
||||
State: instance.State,
|
||||
ConfigVersion: instance.ConfigVersion,
|
||||
ConfigKey: instance.ConfigKey,
|
||||
ConfigContent: instance.ConfigContent,
|
||||
ConfigChecksum: instance.ConfigChecksum,
|
||||
ConfigUpdatedAt: instance.ConfigUpdatedAt,
|
||||
CreatedAt: instance.CreatedAt,
|
||||
UpdatedAt: instance.UpdatedAt,
|
||||
ID: instance.ID,
|
||||
PluginID: instance.PluginID,
|
||||
PluginVersion: instance.PluginVersion,
|
||||
DeploymentTargetID: instance.DeploymentTargetID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Name: instance.Name,
|
||||
OwnerUserID: instance.OwnerUserID,
|
||||
AdminUserIDs: domain.CopyStringSlice(instance.AdminUserIDs),
|
||||
State: instance.State,
|
||||
ConfigVersion: instance.ConfigVersion,
|
||||
ConfigKey: instance.ConfigKey,
|
||||
ConfigContent: instance.ConfigContent,
|
||||
ConfigChecksum: instance.ConfigChecksum,
|
||||
ConfigUpdatedAt: instance.ConfigUpdatedAt,
|
||||
CreatedAt: instance.CreatedAt,
|
||||
UpdatedAt: instance.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (instance ServerInstance) ToDomain() domain.ServerInstance {
|
||||
return domain.ServerInstance{
|
||||
ID: instance.ID,
|
||||
PluginID: instance.PluginID,
|
||||
PluginVersion: instance.PluginVersion,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Name: instance.Name,
|
||||
OwnerUserID: instance.OwnerUserID,
|
||||
AdminUserIDs: domain.CopyStringSlice(instance.AdminUserIDs),
|
||||
State: instance.State,
|
||||
ConfigVersion: instance.ConfigVersion,
|
||||
ConfigKey: instance.ConfigKey,
|
||||
ConfigContent: instance.ConfigContent,
|
||||
ConfigChecksum: instance.ConfigChecksum,
|
||||
ConfigUpdatedAt: instance.ConfigUpdatedAt,
|
||||
CreatedAt: instance.CreatedAt,
|
||||
UpdatedAt: instance.UpdatedAt,
|
||||
ID: instance.ID,
|
||||
PluginID: instance.PluginID,
|
||||
PluginVersion: instance.PluginVersion,
|
||||
DeploymentTargetID: instance.DeploymentTargetID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Name: instance.Name,
|
||||
OwnerUserID: instance.OwnerUserID,
|
||||
AdminUserIDs: domain.CopyStringSlice(instance.AdminUserIDs),
|
||||
State: instance.State,
|
||||
ConfigVersion: instance.ConfigVersion,
|
||||
ConfigKey: instance.ConfigKey,
|
||||
ConfigContent: instance.ConfigContent,
|
||||
ConfigChecksum: instance.ConfigChecksum,
|
||||
ConfigUpdatedAt: instance.ConfigUpdatedAt,
|
||||
CreatedAt: instance.CreatedAt,
|
||||
UpdatedAt: instance.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Named control DTOs:
|
||||
- `RunCapabilityReport`
|
||||
- `RunCapacityReport`
|
||||
|
||||
Control payloads must remain small and must not include logs, artifact chunks, host paths, raw credentials, direct sockets, or long task results. Hello creates or updates run endpoint metadata and issues an in-memory platform session token. Heartbeat requires that active session token and may request capability refresh when the fingerprint changes.
|
||||
Control payloads must remain small and must not include logs, artifact chunks, host paths, raw credentials, direct sockets, or long task results. Hello creates or updates run endpoint metadata and issues an in-memory platform session token. A server-scoped generated Run must use the endpoint identity reserved for its server; Platform rejects a valid component key presented for another endpoint. Heartbeat requires that active session token and may request capability refresh when the fingerprint changes.
|
||||
|
||||
Capacity reports include bounded `maxJobs`, `runningJobs`, `queuedJobs`, `logBacklogBatches`, `artifactBacklogChunks`, and enumerated pressure codes. They report queue and spool counts only, never log bodies, artifact chunks, machine paths, PIDs, sockets, credentials, or transport endpoints.
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
if hasComponentAuthIdentity(hello) {
|
||||
if err := svc.validateDedicatedRunHello(hello); err != nil {
|
||||
return domain.RunControlHelloResult{}, err
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: hello.ServerInstanceID,
|
||||
ComponentKind: hello.ComponentKind,
|
||||
@@ -122,6 +125,32 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) validateDedicatedRunHello(hello domain.RunControlHello) error {
|
||||
if hello.ComponentKind != domain.DistributionComponentRun {
|
||||
return validationError("component-authenticated run hello must use the run component")
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(hello.ServerInstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(instance.DeploymentTargetID) == "" {
|
||||
return nil // legacy Run registrations keep their historical endpoint contract.
|
||||
}
|
||||
if hello.PluginID != instance.PluginID || hello.RunEndpointID != instance.RunEndpointID {
|
||||
return validationError("run endpoint identity does not match the server binding")
|
||||
}
|
||||
instances, err := svc.store.ServerInstances().List(domain.ServerInstanceFilter{RunEndpointID: hello.RunEndpointID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, candidate := range instances {
|
||||
if candidate.ID != instance.ID && candidate.State != domain.ServerInstanceStateDeleted {
|
||||
return validationError("run endpoint is already bound to another server")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasComponentAuthIdentity(hello domain.RunControlHello) bool {
|
||||
return hello.ServerInstanceID != "" || hello.PluginID != "" || hello.ComponentKind != "" || hello.ComponentKey != "" || hello.KeyGeneration != 0
|
||||
}
|
||||
@@ -214,6 +243,15 @@ func (svc *CoreService) revokeRunControlSessionForInstance(instance domain.Serve
|
||||
if strings.TrimSpace(instance.RunEndpointID) == "" {
|
||||
return nil
|
||||
}
|
||||
instances, err := svc.store.ServerInstances().List(domain.ServerInstanceFilter{RunEndpointID: instance.RunEndpointID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, candidate := range instances {
|
||||
if candidate.ID != instance.ID && candidate.State != domain.ServerInstanceStateDeleted {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
svc.controlMu.Lock()
|
||||
defer svc.controlMu.Unlock()
|
||||
|
||||
|
||||
@@ -32,6 +32,31 @@ func TestCoreServiceRegistersNewRunControlSession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceDoesNotRevokeSharedLegacyRunEndpoint(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
hello := validRunControlHello()
|
||||
hello.CapabilityReport.Capabilities = append(hello.CapabilityReport.Capabilities, plugin.RequiredRunCapabilities...)
|
||||
registered, err := svc.RegisterRunHello(hello)
|
||||
if err != nil || !registered.Accepted {
|
||||
t.Fatalf("register shared endpoint: result=%+v err=%v", registered, err)
|
||||
}
|
||||
first, err := svc.CreateServerInstance(domain.ServerInstance{ID: "shared-first", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Shared First", State: domain.ServerInstanceStateReady})
|
||||
if err != nil {
|
||||
t.Fatalf("create first legacy server: %v", err)
|
||||
}
|
||||
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "shared-second", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Shared Second", State: domain.ServerInstanceStateReady}); err != nil {
|
||||
t.Fatalf("create second legacy server: %v", err)
|
||||
}
|
||||
if err := svc.revokeRunControlSessionForInstance(first); err != nil {
|
||||
t.Fatalf("revoke first legacy Run: %v", err)
|
||||
}
|
||||
session, err := svc.store.RunControlSessions().Get(endpoint.ID)
|
||||
if err != nil || session.Status != domain.AuthSessionStatusActive {
|
||||
t.Fatalf("shared legacy endpoint session must remain active, session=%+v err=%v", session, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceReRegistersExistingRunEndpoint(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
first, err := svc.RegisterRunHello(validRunControlHello())
|
||||
|
||||
@@ -44,7 +44,11 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "run.generate.denied"); err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
builderEndpointID := instance.RunEndpointID
|
||||
if strings.TrimSpace(instance.DeploymentTargetID) != "" {
|
||||
builderEndpointID = instance.DeploymentTargetID
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(builderEndpointID)
|
||||
if err != nil {
|
||||
return domain.RunDistribution{}, err
|
||||
}
|
||||
@@ -99,7 +103,7 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: buildJobID,
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
RunEndpointID: builderEndpointID,
|
||||
Capability: domain.JobCapabilityDistributionBuild,
|
||||
TargetKey: "distribution/run",
|
||||
InputRef: "input://distribution-build/" + distribution.ID,
|
||||
@@ -434,9 +438,13 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
||||
if err != nil {
|
||||
return domain.ServerRuntimeActions{}, err
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.ServerRuntimeActions{}, err
|
||||
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 {
|
||||
return domain.ServerRuntimeActions{}, endpointErr
|
||||
}
|
||||
hasAvailableRunPackage := false
|
||||
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: instance.ID, Status: domain.DistributionStatusAvailable})
|
||||
@@ -465,23 +473,30 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
||||
actions := domain.ServerRuntimeActions{
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: endpoint.ID,
|
||||
RunStatus: endpoint.Status,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
RunStatus: func() domain.RunEndpointStatus {
|
||||
if runRegistered {
|
||||
return endpoint.Status
|
||||
}
|
||||
return domain.RunEndpointStatusOffline
|
||||
}(),
|
||||
Actions: []domain.ServerRuntimeAction{
|
||||
runtimeAction("generate-run", "Generate run", pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityDistributionBuild) && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityDistributionBuild), "run endpoint cannot build distributions", bindingReason)),
|
||||
runtimeAction("download-run", "Download run", hasAvailableRunPackage, "run package has not been generated"),
|
||||
runtimeAction("push-run-update", "Push run update", pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", bindingReason)),
|
||||
runtimeAction("push-run-update", "Push run update", runRegistered && pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", bindingReason))),
|
||||
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") && svc.endpointSupports(endpoint, domain.JobCapabilityDistributionBuild) && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.client-manager.manage") || !svc.endpointSupports(endpoint, domain.JobCapabilityDistributionBuild), "run endpoint cannot build distributions", 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", dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck) && bindingsComplete, fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck), "run endpoint cannot check dependencies", bindingReason))),
|
||||
runtimeAction("dependencies-install", "Install dependencies", dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall) && bindingsComplete, fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall), "run endpoint cannot install dependencies", bindingReason))),
|
||||
runtimeAction("live-logs", "Live logs", pluginSupports(plugin, "logs.read"), "plugin does not declare live logs"),
|
||||
runtimeAction("historical-logs", "Historical logs", svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill) && bindingsComplete, 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, "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))),
|
||||
},
|
||||
}
|
||||
actions.Actions = append(actions.Actions, svc.clientManagerRuntimeActionProjection(instance, plugin, endpoint, bindingsComplete, bindingReason)...)
|
||||
if runRegistered {
|
||||
actions.Actions = append(actions.Actions, svc.clientManagerRuntimeActionProjection(instance, plugin, endpoint, bindingsComplete, bindingReason)...)
|
||||
}
|
||||
return domain.CopyServerRuntimeActions(actions), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,26 @@ func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceBuildsDedicatedRunOnDeploymentTarget(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
targetID := instance.RunEndpointID
|
||||
instance.State = domain.ServerInstanceStateDraft
|
||||
instance.DeploymentTargetID = targetID
|
||||
instance.RunEndpointID = "server-run-" + instance.ID
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
t.Fatalf("prepare target-bound draft: %v", err)
|
||||
}
|
||||
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "linux", TargetArch: "amd64", IdempotencyKey: "dedicated-target-build"})
|
||||
if err != nil {
|
||||
t.Fatalf("generate dedicated Run: %v", err)
|
||||
}
|
||||
job, err := svc.GetJob(distribution.BuildJobID)
|
||||
if err != nil || job.RunEndpointID != targetID || distribution.RunEndpointID != instance.RunEndpointID {
|
||||
t.Fatalf("expected build on target %q for dedicated Run %q, job=%+v distribution=%+v err=%v", targetID, instance.RunEndpointID, job, distribution, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRejectsDistributionBuildForStaleRunEndpoint(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
svc.now = func() time.Time { return fixedTime.Add(capacityHeartbeatStaleAfter + time.Second) }
|
||||
|
||||
@@ -2323,7 +2323,8 @@ func validateJobServerTarget(job domain.Job, instance domain.ServerInstance, plu
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return validationError("server instance must not be deleted")
|
||||
}
|
||||
if instance.RunEndpointID != job.RunEndpointID {
|
||||
usesDeploymentTarget := job.Capability == domain.JobCapabilityDistributionBuild && instance.DeploymentTargetID != "" && instance.DeploymentTargetID == job.RunEndpointID
|
||||
if instance.RunEndpointID != job.RunEndpointID && !usesDeploymentTarget {
|
||||
return validationError("job runEndpointId must match server instance")
|
||||
}
|
||||
if plugin.ID != instance.PluginID {
|
||||
|
||||
@@ -96,6 +96,12 @@ func (svc *CoreService) DeployServerInstanceForSession(sessionID string, command
|
||||
if strings.TrimSpace(instance.RunEndpointID) == "" {
|
||||
return domain.ServerLifecycleResult{}, validationError("run endpoint must be selected before deployment")
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
|
||||
@@ -53,6 +53,11 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
||||
UpdatedAt: stamp,
|
||||
Deployment: create.Deployment,
|
||||
}
|
||||
if strings.TrimSpace(create.DeploymentTargetID) != "" {
|
||||
instance.DeploymentTargetID = create.DeploymentTargetID
|
||||
instance.RunEndpointID = dedicatedRunEndpointID(create.ID)
|
||||
instance.State = domain.ServerInstanceStateDraft
|
||||
}
|
||||
if instance.Deployment.Mode != "" {
|
||||
instance.Deployment.ProfileKey = create.ProfileKey
|
||||
instance.Deployment.RuntimeBindings = domain.CopyStringMap(create.Bindings)
|
||||
@@ -71,6 +76,35 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
||||
instance.ConfigContent = buildLogicalServerConfig(instance)
|
||||
instance.ConfigChecksum = validator.BytesChecksum([]byte(instance.ConfigContent))
|
||||
instance.ConfigUpdatedAt = stamp
|
||||
if strings.TrimSpace(create.DeploymentTargetID) != "" {
|
||||
target, targetErr := svc.store.RunEndpoints().Get(create.DeploymentTargetID)
|
||||
if targetErr != nil {
|
||||
return domain.ServerLifecycleResult{}, fmt.Errorf("get deployment target dependency: %w", targetErr)
|
||||
}
|
||||
if err := svc.validateRunnableEndpoint(target, domain.JobCapabilityDistributionBuild); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if err := validator.ValidateServerInstance(instance); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
var binding domain.RuntimeBinding
|
||||
if strings.TrimSpace(create.ProfileKey) != "" {
|
||||
var bindingErr error
|
||||
binding, bindingErr = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true)
|
||||
if bindingErr != nil {
|
||||
return domain.ServerLifecycleResult{}, bindingErr
|
||||
}
|
||||
}
|
||||
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if binding.ID != "" {
|
||||
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
}
|
||||
return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance}), nil
|
||||
}
|
||||
if strings.TrimSpace(create.RunEndpointID) == "" {
|
||||
instance.State = domain.ServerInstanceStateDraft
|
||||
}
|
||||
@@ -142,6 +176,10 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
|
||||
}), nil
|
||||
}
|
||||
|
||||
func dedicatedRunEndpointID(serverInstanceID string) string {
|
||||
return "server-run-" + serverInstanceID
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateServerInstanceWorkflowForSession(sessionID string, create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) {
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,6 +85,46 @@ func TestCoreServiceServerLifecycleWorkflows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceCreatesTargetBoundDraftAndRequiresDedicatedRunRegistration(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",
|
||||
})
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceFreezesReadyDLLExtensionIntoWindowsStartJob(t *testing.T) {
|
||||
svc, sessionToken := newLifecycleRunService(t)
|
||||
setLifecycleEndpointTarget(t, svc, "windows", "amd64")
|
||||
|
||||
Reference in New Issue
Block a user