feat: 完整游戏运维功能
This commit is contained in:
+306
-100
@@ -31,6 +31,7 @@ type Core interface {
|
||||
RegisterUser(domain.UserRegistration) (domain.AuthSession, error)
|
||||
LoginUser(domain.UserLogin) (domain.AuthSession, error)
|
||||
LogoutUser(string) error
|
||||
RotateUserSession(string) (domain.AuthSession, error)
|
||||
GetCurrentUser(string) (domain.User, error)
|
||||
UpdateCurrentUserProfile(string, domain.UserProfile) (domain.User, error)
|
||||
UpdateCurrentUserTheme(string, domain.UserThemePreference) (domain.UserThemePreference, error)
|
||||
@@ -50,12 +51,14 @@ type Core interface {
|
||||
GetMarketplacePlugin(string) (domain.PluginMarketplacePlugin, error)
|
||||
SetMarketplacePluginState(string, domain.PluginMarketplaceStateAction) (domain.PluginMarketplacePlugin, error)
|
||||
AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest) (domain.PluginBridgeAuthorization, error)
|
||||
AuthorizePluginBridgeActionForSession(string, domain.PluginBridgeAuthorizeRequest) (domain.PluginBridgeAuthorization, error)
|
||||
ExecutePluginBridgeAction(string, domain.PluginBridgeExecuteRequest) (domain.PluginBridgeExecuteResponse, error)
|
||||
CreateRunEndpoint(domain.RunEndpoint) (domain.RunEndpoint, error)
|
||||
GetRunEndpoint(string) (domain.RunEndpoint, error)
|
||||
ListRunEndpoints(domain.RunEndpointFilter) ([]domain.RunEndpoint, error)
|
||||
RegisterRunHello(domain.RunControlHello) (domain.RunControlHelloResult, error)
|
||||
AcceptRunHeartbeat(domain.RunControlHeartbeat) (domain.RunControlHeartbeatResult, error)
|
||||
AuthorizeRunRequestSignature(domain.RunRequestSignature) error
|
||||
CreateServerInstance(domain.ServerInstance) (domain.ServerInstance, error)
|
||||
CreateServerInstanceForSession(string, domain.ServerInstance) (domain.ServerInstance, error)
|
||||
CreateServerInstanceWorkflow(domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error)
|
||||
@@ -64,6 +67,7 @@ type Core interface {
|
||||
StartServerInstanceForSession(string, domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
|
||||
StopServerInstance(domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
|
||||
StopServerInstanceForSession(string, domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
|
||||
QueryServerInstanceProcessForSession(string, domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
|
||||
GetServerInstance(string) (domain.ServerInstance, error)
|
||||
GetServerInstanceForSession(string, string) (domain.ServerInstance, error)
|
||||
UpdateServerInstanceForSession(string, string, domain.ServerInstanceUpdate) (domain.ServerInstance, error)
|
||||
@@ -75,6 +79,13 @@ type Core interface {
|
||||
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
|
||||
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
|
||||
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
|
||||
IngestMetricBatch(domain.MetricBatchIngest) (domain.MetricBatchIngestResult, error)
|
||||
ListMetricSamplesForSession(string, domain.MetricSampleFilter) ([]domain.MetricSample, error)
|
||||
CreateBackupForSession(string, domain.BackupRecord) (domain.BackupRecord, error)
|
||||
GetBackupForSession(string, string) (domain.BackupRecord, error)
|
||||
ListBackupsForSession(string, domain.BackupFilter) ([]domain.BackupRecord, error)
|
||||
ListRemoteAdapterDeclarationsForSession(string, string) ([]domain.RemoteAdapterDeclaration, error)
|
||||
RequestRemoteAdapterForSession(string, domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error)
|
||||
GetServerConfigForSession(string, string) (domain.ServerConfig, error)
|
||||
PreviewServerConfigWriteForSession(string, domain.ServerConfigDiffRequest) (domain.ServerConfigDiffPreview, error)
|
||||
ApproveServerConfigWriteForSession(string, domain.ServerConfigWriteApproval) (domain.ServerConfigWriteDispatch, error)
|
||||
@@ -82,28 +93,53 @@ type Core interface {
|
||||
CreateJob(domain.Job) (domain.Job, error)
|
||||
GetJob(string) (domain.Job, error)
|
||||
ListJobs(domain.JobFilter) ([]domain.Job, error)
|
||||
GetJobForSession(string, string) (domain.Job, error)
|
||||
ListJobsForSession(string, domain.JobFilter) ([]domain.Job, error)
|
||||
RequestRunJobCancelForSession(string, domain.RunJobCancelRequest) (domain.RunJobCancelRequestResult, error)
|
||||
ClaimRunJob(domain.RunJobClaim) (domain.RunJobClaimResult, error)
|
||||
AckRunJob(domain.RunJobAck) (domain.RunJobAckResult, error)
|
||||
UpdateRunJobProgress(domain.RunJobProgress) (domain.RunJobProgressResult, error)
|
||||
CompleteRunJob(domain.RunJobResult) (domain.RunJobResultResult, error)
|
||||
GetDistributionBuildInput(domain.DistributionBuildInputRequest) (domain.DistributionBuildInput, error)
|
||||
GetDependencyExecutionInput(domain.DependencyExecutionInputRequest) (domain.DependencyExecutionInput, error)
|
||||
GetRunUpdateInput(domain.RunUpdateInputRequest) (domain.RunUpdateInput, error)
|
||||
ReadRunUpdateChunk(domain.RunUpdateChunkRequest) (domain.RunUpdateChunk, error)
|
||||
ReportRunUpdateHealth(domain.RunUpdateHealthReport) (domain.RunUpdateHealthResult, error)
|
||||
RequestRunJobCancel(domain.RunJobCancelRequest) (domain.RunJobCancelRequestResult, error)
|
||||
PollRunJobCancel(domain.RunJobCancelPoll) (domain.RunJobCancelPollResult, error)
|
||||
ReconcileRunJobs(domain.RunJobReconcile) (domain.RunJobReconcileResult, error)
|
||||
CreateArtifact(domain.Artifact) (domain.Artifact, error)
|
||||
GetArtifact(string) (domain.Artifact, error)
|
||||
ListArtifacts(domain.ArtifactFilter) ([]domain.Artifact, error)
|
||||
ListArtifactsForSession(string, domain.ArtifactFilter) ([]domain.Artifact, error)
|
||||
GetArtifactForSession(string, string) (domain.Artifact, error)
|
||||
OpenArtifactDownloadForSession(string, domain.ArtifactDownloadReferenceRequest) (domain.ArtifactDownloadReference, error)
|
||||
ReadArtifactContentForSession(string, domain.ArtifactContentRequest) (domain.ArtifactContent, error)
|
||||
GetServerRuntimeActionsForSession(string, string) (domain.ServerRuntimeActions, error)
|
||||
GetServerRuntimeBindingForSession(string, string) (domain.RuntimeBindingView, error)
|
||||
UpdateServerRuntimeBindingForSession(string, string, domain.RuntimeBindingUpdate) (domain.RuntimeBindingView, error)
|
||||
GenerateRunDistributionForSession(string, domain.RunDistributionGenerateRequest) (domain.RunDistribution, error)
|
||||
GenerateClientManagerDistributionForSession(string, domain.ClientManagerBuildRequest) (domain.ClientManagerDistribution, error)
|
||||
OpenLatestRunDistributionDownloadForSession(string, string) (domain.ArtifactDownloadReference, error)
|
||||
OpenLatestClientManagerDistributionDownloadForSession(string, string, string) (domain.ArtifactDownloadReference, error)
|
||||
ResetComponentKeyForSession(string, domain.ComponentKeyResetRequest) (domain.EncryptedComponentKey, error)
|
||||
AuthenticateComponent(domain.ComponentAuthenticationRequest) (domain.ComponentAuthenticationResult, error)
|
||||
DeployClientManagerForSession(string, domain.ClientManagerDeployRequest) (domain.ClientManagerLifecycleView, error)
|
||||
ControlClientManagerForSession(string, domain.ClientManagerControlRequest) (domain.ClientManagerLifecycleView, error)
|
||||
UpdateClientManagerForSession(string, domain.ClientManagerUpdateRequest) (domain.ClientManagerLifecycleView, error)
|
||||
UninstallClientManagerForSession(string, domain.ClientManagerUninstallRequest) (domain.ClientManagerLifecycleView, error)
|
||||
RetryClientManagerLifecycleForSession(string, domain.ClientManagerRetryRequest) (domain.ClientManagerLifecycleView, error)
|
||||
RevokeClientManagerSessionForSession(string, domain.ClientManagerRevokeSessionRequest) (domain.ClientManagerLifecycleView, error)
|
||||
GetClientManagerLifecycleForSession(string, string, string) (domain.ClientManagerLifecycleView, error)
|
||||
ListClientManagerLifecyclesForSession(string, string) ([]domain.ClientManagerLifecycleView, error)
|
||||
GetClientManagerLifecycleInput(domain.ClientManagerLifecycleInputRequest) (domain.ClientManagerLifecycleInput, error)
|
||||
ReadClientManagerLifecycleChunk(domain.RunUpdateChunkRequest) (domain.RunUpdateChunk, error)
|
||||
RegisterClientManager(domain.ClientManagerRegisterRequest) (domain.ClientManagerRegisterResult, error)
|
||||
AcceptClientManagerHeartbeat(domain.ClientManagerHeartbeat) (domain.ClientManagerHeartbeatResult, error)
|
||||
ReconcileClientManagerLifecycle() error
|
||||
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
|
||||
ListRunUpdateJobsForSession(string, string) ([]domain.RunUpdateJob, error)
|
||||
GetDependencyCatalogForSession(string, string) (domain.DependencyCatalog, error)
|
||||
QueueDependencyJobForSession(string, domain.DependencyJobRequest) (domain.Job, error)
|
||||
QueueLogBackfillForSession(string, domain.LogBackfillRequest) (domain.Job, error)
|
||||
OpenArtifactTransfer(domain.ArtifactTransferOpen) (domain.ArtifactTransferOpenResult, error)
|
||||
@@ -113,11 +149,15 @@ type Core interface {
|
||||
CreateLogStream(domain.LogStream) (domain.LogStream, error)
|
||||
GetLogStream(string) (domain.LogStream, error)
|
||||
ListLogStreams(domain.LogStreamFilter) ([]domain.LogStream, error)
|
||||
GetLogStreamForSession(string, string) (domain.LogStream, error)
|
||||
ListLogStreamsForSession(string, domain.LogStreamFilter) ([]domain.LogStream, error)
|
||||
QueryLogStreamForSession(string, domain.LogStreamCursorQuery) (domain.LogStreamCursorResult, error)
|
||||
IngestLogBatch(domain.LogBatchIngest) (domain.LogBatchIngestResult, error)
|
||||
QueryLogStream(domain.LogStreamCursorQuery) (domain.LogStreamCursorResult, error)
|
||||
CreateAuditEvent(domain.AuditEvent) (domain.AuditEvent, error)
|
||||
GetAuditEvent(string) (domain.AuditEvent, error)
|
||||
ListAuditEvents(domain.AuditEventFilter) ([]domain.AuditEvent, error)
|
||||
SeedPlatformAdmin(string, string) error
|
||||
}
|
||||
|
||||
type CoreService struct {
|
||||
@@ -129,9 +169,8 @@ type CoreService struct {
|
||||
runSessions map[string]domain.RunControlSession
|
||||
runSessionSeq uint64
|
||||
jobMu sync.Mutex
|
||||
jobLeases map[string]domain.RunJobLease
|
||||
jobLeaseSeq uint64
|
||||
logStore LogBodyStore
|
||||
artifactStore ArtifactBodyStore
|
||||
artifactMu sync.Mutex
|
||||
artifactTransfers map[string]domain.ArtifactTransferSession
|
||||
artifactPayloads map[string][]byte
|
||||
@@ -139,6 +178,7 @@ type CoreService struct {
|
||||
auditMu sync.Mutex
|
||||
auditSeq uint64
|
||||
aiProviderClient AIProviderClient
|
||||
secretEnvelope SecretEnvelope
|
||||
}
|
||||
|
||||
var _ Core = (*CoreService)(nil)
|
||||
@@ -159,17 +199,71 @@ func newCoreServiceWithLogStore(store repo.Store, logStore LogBodyStore, now fun
|
||||
if logStore == nil {
|
||||
logStore = NewMemoryLogBodyStore()
|
||||
}
|
||||
return &CoreService{
|
||||
artifactStore := NewMemoryArtifactBodyStore()
|
||||
service := &CoreService{
|
||||
store: store,
|
||||
now: now,
|
||||
authSessions: map[string]string{},
|
||||
runSessions: map[string]domain.RunControlSession{},
|
||||
jobLeases: map[string]domain.RunJobLease{},
|
||||
logStore: logStore,
|
||||
artifactStore: artifactStore,
|
||||
artifactTransfers: map[string]domain.ArtifactTransferSession{},
|
||||
artifactPayloads: map[string][]byte{},
|
||||
aiProviderClient: MockAIProviderClient{},
|
||||
secretEnvelope: newSecretEnvelope(developmentSecretEnvelopeKey),
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
func NewCoreServiceWithDurableStores(store repo.Store, logStore LogBodyStore, artifactStore ArtifactBodyStore) (*CoreService, error) {
|
||||
if artifactStore == nil {
|
||||
artifactStore = NewMemoryArtifactBodyStore()
|
||||
}
|
||||
service := newCoreServiceWithLogStore(store, logStore, func() time.Time { return time.Now().UTC() })
|
||||
service.artifactStore = artifactStore
|
||||
sessions, err := artifactStore.LoadTransfers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, session := range sessions {
|
||||
service.artifactTransfers[session.TransferID] = domain.CopyArtifactTransferSession(session)
|
||||
}
|
||||
if err := service.recoverLogCursors(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := service.RecoverIncompleteBackups(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := service.ReconcileClientManagerLifecycle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) recoverLogCursors() error {
|
||||
store, ok := svc.logStore.(interface{ LatestSeq(string) (uint64, error) })
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
streams, err := svc.store.LogStreams().List(domain.LogStreamFilter{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stream := range streams {
|
||||
latest, latestErr := store.LatestSeq(stream.ID)
|
||||
if latestErr != nil || latest <= stream.LatestSeq {
|
||||
if latestErr != nil {
|
||||
return latestErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
stream.LatestSeq = latest
|
||||
stream.UpdatedAt = svc.now()
|
||||
if err := svc.store.LogStreams().Update(stream); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateUser(user domain.User) (domain.User, error) {
|
||||
@@ -240,6 +334,11 @@ func (svc *CoreService) UpdateUser(id string, user domain.User) (domain.User, er
|
||||
if err := svc.store.Users().Update(user); err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
if user.Status != domain.UserStatusActive {
|
||||
if err := svc.revokeUserSessions(user.ID); err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
}
|
||||
return domain.CopyUser(user), nil
|
||||
}
|
||||
|
||||
@@ -282,19 +381,7 @@ func (svc *CoreService) RegisterUser(registration domain.UserRegistration) (doma
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
if firstUser {
|
||||
sessionID, err := randomToken()
|
||||
if err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
svc.authMu.Lock()
|
||||
svc.authSessions[sessionID] = created.ID
|
||||
svc.authMu.Unlock()
|
||||
return domain.AuthSession{
|
||||
SessionID: sessionID,
|
||||
User: created,
|
||||
Status: "authenticated",
|
||||
Message: "首个账号已创建为平台管理员。",
|
||||
}, nil
|
||||
return svc.issueAuthSession(created, "首个账号已创建为平台管理员。")
|
||||
}
|
||||
return domain.AuthSession{
|
||||
User: created,
|
||||
@@ -325,27 +412,11 @@ func (svc *CoreService) LoginUser(login domain.UserLogin) (domain.AuthSession, e
|
||||
if matched.Status == domain.UserStatusDisabled {
|
||||
return domain.AuthSession{}, ErrForbidden
|
||||
}
|
||||
sessionID, err := randomToken()
|
||||
if err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
svc.authMu.Lock()
|
||||
svc.authSessions[sessionID] = matched.ID
|
||||
svc.authMu.Unlock()
|
||||
return domain.AuthSession{SessionID: sessionID, User: matched, Status: "authenticated", Message: "登录成功"}, nil
|
||||
return svc.issueAuthSession(matched, "登录成功")
|
||||
}
|
||||
|
||||
func (svc *CoreService) LogoutUser(sessionID string) error {
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
return ErrUnauthorized
|
||||
}
|
||||
svc.authMu.Lock()
|
||||
defer svc.authMu.Unlock()
|
||||
if _, exists := svc.authSessions[sessionID]; !exists {
|
||||
return ErrUnauthorized
|
||||
}
|
||||
delete(svc.authSessions, sessionID)
|
||||
return nil
|
||||
return svc.revokeAuthSession(sessionID)
|
||||
}
|
||||
|
||||
func (svc *CoreService) GetCurrentUser(sessionID string) (domain.User, error) {
|
||||
@@ -381,7 +452,17 @@ func (svc *CoreService) UpdateCurrentUserTheme(sessionID string, preference doma
|
||||
}
|
||||
|
||||
func (svc *CoreService) SeedLocalPlatformAdmin() error {
|
||||
const adminEmail = "operator.local@example.test"
|
||||
return svc.SeedPlatformAdmin("operator.local@example.test", "operator-local")
|
||||
}
|
||||
|
||||
func (svc *CoreService) SeedPlatformAdmin(email string, password string) error {
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
email = "operator.local@example.test"
|
||||
}
|
||||
if len([]rune(password)) < 12 {
|
||||
return validationError("bootstrap admin password must be at least 12 characters")
|
||||
}
|
||||
_, err := svc.store.Users().Get("user-admin")
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -392,11 +473,11 @@ func (svc *CoreService) SeedLocalPlatformAdmin() error {
|
||||
return svc.store.Users().Create(domain.User{
|
||||
ID: "user-admin",
|
||||
DisplayName: "Operator",
|
||||
Email: adminEmail,
|
||||
Email: email,
|
||||
Status: domain.UserStatusActive,
|
||||
Roles: []string{"platform-admin"},
|
||||
PasswordHash: mustHashPassword("operator-local"),
|
||||
Profile: domain.UserProfile{ContactNote: "local development admin"},
|
||||
PasswordHash: mustHashPassword(password),
|
||||
Profile: domain.UserProfile{ContactNote: "bootstrap platform admin"},
|
||||
CreatedAt: svc.now(),
|
||||
UpdatedAt: svc.now(),
|
||||
})
|
||||
@@ -541,6 +622,7 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
|
||||
Tags: manifest.Tags,
|
||||
AIPurposes: manifest.AI.Purposes,
|
||||
RemoteAccess: manifest.RemoteAccess,
|
||||
RuntimeProfiles: manifest.RuntimeProfiles,
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
}
|
||||
@@ -623,11 +705,11 @@ func (svc *CoreService) ExecutePluginBridgeAction(sessionID string, request doma
|
||||
case domain.PluginBridgeActionFilesRequest:
|
||||
base = svc.executeBridgeFileRequest(sessionID, base, request)
|
||||
case domain.PluginBridgeActionRemoteAccessRequest:
|
||||
base = svc.executeBridgeRemoteAccessRequest(base, plugin, instance, request.Payload)
|
||||
base = svc.executeBridgeRemoteAccessRequest(sessionID, base, plugin, instance, request.Payload)
|
||||
case domain.PluginBridgeActionRunDistribution:
|
||||
base = svc.executeBridgeRunDistribution(sessionID, base, request)
|
||||
case domain.PluginBridgeActionDependenciesRequest:
|
||||
base = svc.executeBridgeDependenciesRequest(base, plugin, instance, request.Payload)
|
||||
base = svc.executeBridgeDependenciesRequest(sessionID, base, plugin, instance, request.Payload)
|
||||
case domain.PluginBridgeActionLogsBackfillRequest:
|
||||
base = svc.executeBridgeLogsBackfillRequest(base, plugin, instance, request.Payload)
|
||||
case domain.PluginBridgeActionClientManager:
|
||||
@@ -843,39 +925,44 @@ func (svc *CoreService) executeBridgeFileRequest(sessionID string, base domain.P
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeRemoteAccessRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
capability := strings.TrimSpace(payload["capability"])
|
||||
if capability == "" {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "capability is required"}
|
||||
return base
|
||||
}
|
||||
if !containsString(plugin.RequiredRunCapabilities, capability) {
|
||||
if !containsString(plugin.RequiredRunCapabilities, capability) || !containsString(plugin.RemoteAccess.RunCapabilities, capability) {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "requested remote capability is not declared by plugin"}
|
||||
return base
|
||||
}
|
||||
job := domain.Job{
|
||||
ID: jobIDFromParts("job-remote", base.RequestID, capability),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: capability,
|
||||
TargetKey: payload["targetKey"],
|
||||
InputRef: payload["inputRef"],
|
||||
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "remote access job queued"},
|
||||
declarationKey := strings.TrimSpace(payload["declarationKey"])
|
||||
if declarationKey == "" {
|
||||
for _, profile := range plugin.RuntimeProfiles.TransportProfiles {
|
||||
if profile.TargetKey == payload["targetKey"] && containsString(profile.Capabilities, capability) {
|
||||
declarationKey = profile.Key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
created, err := svc.CreateJob(job)
|
||||
if declarationKey == "" {
|
||||
declarationKey = "legacy-" + string(remoteAdapterKindForCapability(capability))
|
||||
}
|
||||
timeoutSeconds, _ := strconv.Atoi(payload["timeoutSeconds"])
|
||||
maxAttempts, _ := strconv.Atoi(payload["maxAttempts"])
|
||||
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: declarationKey, TargetKey: payload["targetKey"], Capability: capability, TimeoutSeconds: timeoutSeconds, MaxAttempts: maxAttempts, IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID)})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "queued"
|
||||
base.Result = map[string]string{
|
||||
"jobId": created.ID,
|
||||
"state": string(created.State),
|
||||
"capability": created.Capability,
|
||||
"targetKey": created.TargetKey,
|
||||
"serverInstanceId": created.ServerInstanceID,
|
||||
"jobId": result.RequestID,
|
||||
"state": result.Status,
|
||||
"capability": capability,
|
||||
"targetKey": result.TargetKey,
|
||||
"serverInstanceId": result.ServerInstanceID,
|
||||
"adapterKind": string(result.Kind),
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -896,60 +983,116 @@ func (svc *CoreService) executeBridgeRunDistribution(sessionID string, base doma
|
||||
"artifactId": distribution.ArtifactID,
|
||||
"checksum": distribution.Checksum,
|
||||
"keyGeneration": strconv.Itoa(distribution.KeyGeneration),
|
||||
"secretRef": distribution.SecretRef,
|
||||
"status": string(distribution.Status),
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeClientManager(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
|
||||
distribution, err := svc.GenerateClientManagerDistributionForSession(sessionID, domain.ClientManagerBuildRequest{
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
ProfileKey: request.Payload["profileKey"],
|
||||
TargetOS: defaultBridgeValue(request.Payload["targetOs"], "windows"),
|
||||
TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"),
|
||||
RepositoryURL: request.Payload["repositoryUrl"],
|
||||
SourceRevision: request.Payload["sourceRevision"],
|
||||
IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID),
|
||||
})
|
||||
profileKey := request.Payload["profileKey"]
|
||||
operation := defaultBridgeValue(request.Payload["operation"], "status")
|
||||
idempotencyKey := defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID)
|
||||
generation, _ := strconv.Atoi(request.Payload["expectedDeploymentGeneration"])
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
profile, err := findRuntimeClientManagerProfile(plugin, profileKey)
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, ErrForbidden)
|
||||
}
|
||||
var view domain.ClientManagerLifecycleView
|
||||
switch operation {
|
||||
case "generate":
|
||||
distribution, err := svc.GenerateClientManagerDistributionForSession(sessionID, domain.ClientManagerBuildRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, TargetOS: defaultBridgeValue(request.Payload["targetOs"], "windows"), TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"), RepositoryURL: profile.RepositoryURL, SourceRevision: clientManagerProfileRevision(profile), IdempotencyKey: idempotencyKey})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "queued"
|
||||
base.Result = map[string]string{"distributionId": distribution.ID, "buildJobId": distribution.BuildJobID, "artifactId": distribution.ArtifactID, "checksum": distribution.Checksum, "keyGeneration": strconv.Itoa(distribution.KeyGeneration), "version": distribution.Version, "status": string(distribution.Status)}
|
||||
return base
|
||||
case "download":
|
||||
reference, err := svc.OpenLatestClientManagerDistributionDownloadForSession(sessionID, instance.ID, profileKey)
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "ok"
|
||||
base.Result = map[string]string{"artifactId": reference.ArtifactID, "downloadUrl": reference.DownloadURL, "checksum": reference.Checksum, "sizeBytes": strconv.FormatInt(reference.SizeBytes, 10), "expiresAt": reference.ExpiresAt.Format(time.RFC3339), "rangeSupported": strconv.FormatBool(reference.RangeSupported), "chunkSizeBytes": strconv.Itoa(reference.ChunkSizeBytes)}
|
||||
return base
|
||||
case "reset-key":
|
||||
key, err := svc.ResetComponentKeyForSession(sessionID, domain.ComponentKeyResetRequest{ServerInstanceID: instance.ID, ComponentKind: domain.DistributionComponentClientManager, ComponentKey: profileKey})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "ok"
|
||||
base.Result = map[string]string{"profileKey": profileKey, "keyGeneration": strconv.Itoa(key.Generation), "status": string(key.Status), "requiresRedeploy": "true"}
|
||||
return base
|
||||
case "status":
|
||||
view, err = svc.GetClientManagerLifecycleForSession(sessionID, instance.ID, profileKey)
|
||||
case "deploy":
|
||||
distributionID, resolveErr := svc.resolveClientManagerDistributionID(instance.ID, profileKey, request.Payload["artifactId"])
|
||||
if resolveErr != nil {
|
||||
return bridgeExecutionError(base, resolveErr)
|
||||
}
|
||||
view, err = svc.DeployClientManagerForSession(sessionID, domain.ClientManagerDeployRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, DistributionID: distributionID, ExpectedDeploymentGeneration: generation, IdempotencyKey: idempotencyKey})
|
||||
case "start", "stop", "restart", "rollback":
|
||||
view, err = svc.ControlClientManagerForSession(sessionID, domain.ClientManagerControlRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, Operation: domain.ClientManagerLifecycleOperation(operation), ExpectedDeploymentGeneration: generation, IdempotencyKey: idempotencyKey})
|
||||
case "update":
|
||||
distributionID, resolveErr := svc.resolveClientManagerDistributionID(instance.ID, profileKey, request.Payload["artifactId"])
|
||||
if resolveErr != nil {
|
||||
return bridgeExecutionError(base, resolveErr)
|
||||
}
|
||||
view, err = svc.UpdateClientManagerForSession(sessionID, domain.ClientManagerUpdateRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, DistributionID: distributionID, ExpectedDeploymentGeneration: generation, Approved: true, IdempotencyKey: idempotencyKey})
|
||||
case "retry":
|
||||
view, err = svc.RetryClientManagerLifecycleForSession(sessionID, domain.ClientManagerRetryRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, ExpectedDeploymentGeneration: generation, IdempotencyKey: idempotencyKey})
|
||||
case "revoke-session":
|
||||
view, err = svc.RevokeClientManagerSessionForSession(sessionID, domain.ClientManagerRevokeSessionRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, Reason: "plugin bridge operator request"})
|
||||
case "uninstall":
|
||||
view, err = svc.UninstallClientManagerForSession(sessionID, domain.ClientManagerUninstallRequest{ServerInstanceID: instance.ID, ProfileKey: profileKey, ExpectedDeploymentGeneration: generation, Confirmed: true, IdempotencyKey: idempotencyKey})
|
||||
default:
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "invalid_client_manager_operation", Message: "client-manager operation is not supported"}
|
||||
return base
|
||||
}
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "ok"
|
||||
base.Result = map[string]string{
|
||||
"distributionId": distribution.ID,
|
||||
"buildJobId": distribution.BuildJobID,
|
||||
"artifactId": distribution.ArtifactID,
|
||||
"checksum": distribution.Checksum,
|
||||
"keyGeneration": strconv.Itoa(distribution.KeyGeneration),
|
||||
"secretRef": distribution.SecretRef,
|
||||
"status": string(distribution.Status),
|
||||
if view.Job.ID != "" && !isTerminalJobState(view.Job.State) {
|
||||
base.Status = "queued"
|
||||
}
|
||||
base.Result = safeClientManagerBridgeResult(view)
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeDependenciesRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
action := defaultBridgeValue(payload["action"], "check")
|
||||
func (svc *CoreService) executeBridgeDependenciesRequest(sessionID string, base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
action := defaultBridgeValue(payload["operation"], "check")
|
||||
capability := domain.JobCapabilityDependenciesCheck
|
||||
message := "dependency check queued"
|
||||
if action == "install" {
|
||||
capability = domain.JobCapabilityDependenciesInstall
|
||||
message = "dependency install queued"
|
||||
} else if action != "check" {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "invalid_dependency_operation", Message: "dependency operation must be check or install"}
|
||||
return base
|
||||
}
|
||||
if !containsString(plugin.RequiredRunCapabilities, capability) {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "dependency capability is not declared by plugin"}
|
||||
return base
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: jobIDFromParts("job-dependencies", base.RequestID, capability),
|
||||
job, err := svc.QueueDependencyJobForSession(sessionID, domain.DependencyJobRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: capability,
|
||||
TargetKey: defaultBridgeValue(payload["probeKey"], "dependencies/default"),
|
||||
InputRef: payload["inputRef"],
|
||||
ProbeKey: payload["probeKey"],
|
||||
InstallPlanKey: payload["planKey"],
|
||||
PlanDigest: payload["planDigest"],
|
||||
TargetOS: payload["targetOS"],
|
||||
TargetArch: payload["targetArch"],
|
||||
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
|
||||
Progress: domain.JobProgress{Percent: 0, Message: message},
|
||||
Install: action == "install",
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
@@ -1136,6 +1279,7 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
|
||||
Tags: plugin.Tags,
|
||||
AIPurposes: plugin.AIPurposes,
|
||||
RemoteAccess: plugin.RemoteAccess,
|
||||
RuntimeProfiles: plugin.RuntimeProfiles,
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
Source: "platform-registry",
|
||||
@@ -1390,11 +1534,22 @@ func (svc *CoreService) GetServerConfigForSession(sessionID string, serverInstan
|
||||
Key: "server.properties",
|
||||
Source: "platform-derived",
|
||||
UpdatedAt: instance.UpdatedAt,
|
||||
Checksum: instance.ConfigChecksum,
|
||||
}
|
||||
if config.UpdatedAt.IsZero() {
|
||||
config.UpdatedAt = svc.now()
|
||||
}
|
||||
config.Content = buildLogicalServerConfig(instance)
|
||||
config.Key = instance.ConfigKey
|
||||
if config.Key == "" {
|
||||
config.Key = "server.properties"
|
||||
}
|
||||
config.Content = instance.ConfigContent
|
||||
if config.Content == "" {
|
||||
config.Content = buildLogicalServerConfig(instance)
|
||||
}
|
||||
if config.Checksum == "" {
|
||||
config.Checksum = validator.BytesChecksum([]byte(config.Content))
|
||||
}
|
||||
if err := validator.ValidateServerConfig(config); err != nil {
|
||||
return domain.ServerConfig{}, err
|
||||
}
|
||||
@@ -1415,12 +1570,16 @@ func (svc *CoreService) PreviewServerConfigWriteForSession(sessionID string, req
|
||||
if config.ConfigVersion != request.ExpectedConfigVersion {
|
||||
return domain.ServerConfigDiffPreview{}, validationError("expectedConfigVersion must match server instance")
|
||||
}
|
||||
if request.ExpectedChecksum != "" && config.Checksum != request.ExpectedChecksum {
|
||||
return domain.ServerConfigDiffPreview{}, validationError("expectedChecksum must match server config")
|
||||
}
|
||||
if config.Key != request.Key {
|
||||
return domain.ServerConfigDiffPreview{}, validationError("key must match server config")
|
||||
}
|
||||
preview := domain.ServerConfigDiffPreview{
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
ConfigVersion: config.ConfigVersion,
|
||||
Checksum: config.Checksum,
|
||||
Key: request.Key,
|
||||
CurrentContent: config.Content,
|
||||
ProposedContent: request.ProposedContent,
|
||||
@@ -1446,6 +1605,7 @@ func (svc *CoreService) ApproveServerConfigWriteForSession(sessionID string, app
|
||||
preview, err := svc.PreviewServerConfigWriteForSession(sessionID, domain.ServerConfigDiffRequest{
|
||||
ServerInstanceID: approval.ServerInstanceID,
|
||||
ExpectedConfigVersion: approval.ExpectedConfigVersion,
|
||||
ExpectedChecksum: approval.ExpectedChecksum,
|
||||
Key: approval.Key,
|
||||
ProposedContent: approval.ProposedContent,
|
||||
ProposedContentInputRef: approval.ProposedContentInputRef,
|
||||
@@ -1460,6 +1620,13 @@ func (svc *CoreService) ApproveServerConfigWriteForSession(sessionID string, app
|
||||
if err != nil {
|
||||
return domain.ServerConfigWriteDispatch{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.ServerConfigWriteDispatch{}, err
|
||||
}
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "config.write.denied"); err != nil {
|
||||
return domain.ServerConfigWriteDispatch{}, err
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: jobIDFromParts("job-config-write", approval.ServerInstanceID, approval.IdempotencyKey),
|
||||
ServerInstanceID: instance.ID,
|
||||
@@ -1467,6 +1634,7 @@ func (svc *CoreService) ApproveServerConfigWriteForSession(sessionID string, app
|
||||
Capability: domain.JobCapabilityConfigWrite,
|
||||
TargetKey: approval.Key,
|
||||
InputRef: approval.ProposedContentInputRef,
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), Content: approval.ProposedContent, ExpectedVersion: approval.ExpectedConfigVersion, ExpectedChecksum: preview.Checksum, MaxReadBytes: 64 * 1024},
|
||||
IdempotencyKey: approval.IdempotencyKey,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "config write queued"},
|
||||
})
|
||||
@@ -1487,6 +1655,29 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
|
||||
if request.ExpectedConfigVersion > 0 && request.ExpectedConfigVersion != instance.ConfigVersion {
|
||||
return domain.FileOperationDispatchResult{}, validationError("expectedConfigVersion must match server instance")
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.FileOperationDispatchResult{}, err
|
||||
}
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "file.operation.denied"); err != nil {
|
||||
return domain.FileOperationDispatchResult{}, err
|
||||
}
|
||||
content := request.Content
|
||||
if request.Operation == domain.FileOperationWrite && content == "" && strings.HasPrefix(request.InputRef, "artifact://") {
|
||||
artifactID := strings.TrimPrefix(request.InputRef, "artifact://")
|
||||
artifact, artifactErr := svc.store.Artifacts().Get(artifactID)
|
||||
if artifactErr != nil {
|
||||
return domain.FileOperationDispatchResult{}, artifactErr
|
||||
}
|
||||
if artifact.OwnerKind != domain.ArtifactOwnerKindServerInstance || artifact.OwnerID != instance.ID || artifact.State != domain.ArtifactStateAvailable {
|
||||
return domain.FileOperationDispatchResult{}, ErrForbidden
|
||||
}
|
||||
payload, payloadErr := svc.artifactPayload(artifactID)
|
||||
if payloadErr != nil {
|
||||
return domain.FileOperationDispatchResult{}, payloadErr
|
||||
}
|
||||
content = string(payload)
|
||||
}
|
||||
if request.PluginID != "" {
|
||||
plugin, err := svc.store.GamePlugins().Get(request.PluginID)
|
||||
if err != nil {
|
||||
@@ -1518,6 +1709,7 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
|
||||
Capability: capability,
|
||||
TargetKey: request.Key,
|
||||
InputRef: request.InputRef,
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), Content: content, ExpectedVersion: request.ExpectedConfigVersion, ExpectedChecksum: request.ExpectedChecksum, MaxReadBytes: 64 * 1024},
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: message},
|
||||
})
|
||||
@@ -1535,6 +1727,14 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) runtimeProfileScope(serverInstanceID string) string {
|
||||
binding, err := svc.runtimeBindingForServer(serverInstanceID)
|
||||
if err != nil {
|
||||
return "default"
|
||||
}
|
||||
return binding.ProfileKey
|
||||
}
|
||||
|
||||
func (svc *CoreService) metricsForServer(instance domain.ServerInstance) domain.ServerMetrics {
|
||||
metrics := domain.ServerMetrics{
|
||||
ServerInstanceID: instance.ID,
|
||||
@@ -1732,6 +1932,7 @@ func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
|
||||
if job.UpdatedAt.IsZero() {
|
||||
job.UpdatedAt = stamp
|
||||
}
|
||||
job = normalizeJobScheduling(job, stamp)
|
||||
if err := validator.ValidateJob(job); err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
@@ -1772,11 +1973,22 @@ func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
|
||||
}
|
||||
|
||||
func (svc *CoreService) GetJob(id string) (domain.Job, error) {
|
||||
return svc.store.Jobs().Get(id)
|
||||
job, err := svc.store.Jobs().Get(id)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
return normalizeJobScheduling(job, svc.now()), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListJobs(filter domain.JobFilter) ([]domain.Job, error) {
|
||||
return svc.store.Jobs().List(filter)
|
||||
jobs, err := svc.store.Jobs().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range jobs {
|
||||
jobs[i] = normalizeJobScheduling(jobs[i], svc.now())
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateArtifact(artifact domain.Artifact) (domain.Artifact, error) {
|
||||
@@ -1891,17 +2103,11 @@ func validationError(violation string) error {
|
||||
}
|
||||
|
||||
func (svc *CoreService) userIDForSession(sessionID string) (string, error) {
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
if sessionID == "" {
|
||||
return "", ErrUnauthorized
|
||||
session, err := svc.authenticatedSession(sessionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
svc.authMu.Lock()
|
||||
defer svc.authMu.Unlock()
|
||||
userID, exists := svc.authSessions[sessionID]
|
||||
if !exists {
|
||||
return "", ErrUnauthorized
|
||||
}
|
||||
return userID, nil
|
||||
return session.UserID, nil
|
||||
}
|
||||
|
||||
func userIDFromEmail(email string) string {
|
||||
|
||||
Reference in New Issue
Block a user