Complete platform management workflows
This commit is contained in:
@@ -66,11 +66,13 @@ type Core interface {
|
||||
StopServerInstanceForSession(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)
|
||||
ListServerInstances(domain.ServerInstanceFilter) ([]domain.ServerInstance, error)
|
||||
ListServerInstancesForSession(string, domain.ServerInstanceFilter) ([]domain.ServerInstance, error)
|
||||
ListServerAdministratorCandidates(string, string) ([]domain.User, error)
|
||||
AddServerAdministrator(string, string, string) (domain.ServerInstance, error)
|
||||
RemoveServerAdministrator(string, string, string) (domain.ServerInstance, error)
|
||||
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
|
||||
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
|
||||
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
|
||||
GetServerConfigForSession(string, string) (domain.ServerConfig, error)
|
||||
@@ -93,6 +95,16 @@ type Core interface {
|
||||
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)
|
||||
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)
|
||||
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
|
||||
QueueDependencyJobForSession(string, domain.DependencyJobRequest) (domain.Job, error)
|
||||
QueueLogBackfillForSession(string, domain.LogBackfillRequest) (domain.Job, error)
|
||||
OpenArtifactTransfer(domain.ArtifactTransferOpen) (domain.ArtifactTransferOpenResult, error)
|
||||
UploadArtifactChunk(domain.ArtifactChunkUpload) (domain.ArtifactChunkUploadResult, error)
|
||||
QueryArtifactTransferStatus(domain.ArtifactTransferStatusQuery) (domain.ArtifactTransferStatusResult, error)
|
||||
@@ -121,7 +133,10 @@ type CoreService struct {
|
||||
logStore LogBodyStore
|
||||
artifactMu sync.Mutex
|
||||
artifactTransfers map[string]domain.ArtifactTransferSession
|
||||
artifactPayloads map[string][]byte
|
||||
artifactTransferSeq uint64
|
||||
auditMu sync.Mutex
|
||||
auditSeq uint64
|
||||
aiProviderClient AIProviderClient
|
||||
}
|
||||
|
||||
@@ -151,6 +166,7 @@ func newCoreServiceWithLogStore(store repo.Store, logStore LogBodyStore, now fun
|
||||
jobLeases: map[string]domain.RunJobLease{},
|
||||
logStore: logStore,
|
||||
artifactTransfers: map[string]domain.ArtifactTransferSession{},
|
||||
artifactPayloads: map[string][]byte{},
|
||||
aiProviderClient: MockAIProviderClient{},
|
||||
}
|
||||
}
|
||||
@@ -523,6 +539,7 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
|
||||
Pages: manifest.Pages,
|
||||
Tags: manifest.Tags,
|
||||
AIPurposes: manifest.AI.Purposes,
|
||||
RemoteAccess: manifest.RemoteAccess,
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
}
|
||||
@@ -604,6 +621,16 @@ func (svc *CoreService) ExecutePluginBridgeAction(sessionID string, request doma
|
||||
base = svc.executeBridgeLogsQuery(base, instance, request.Payload)
|
||||
case domain.PluginBridgeActionFilesRequest:
|
||||
base = svc.executeBridgeFileRequest(sessionID, base, request)
|
||||
case domain.PluginBridgeActionRemoteAccessRequest:
|
||||
base = svc.executeBridgeRemoteAccessRequest(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)
|
||||
case domain.PluginBridgeActionLogsBackfillRequest:
|
||||
base = svc.executeBridgeLogsBackfillRequest(base, plugin, instance, request.Payload)
|
||||
case domain.PluginBridgeActionClientManager:
|
||||
base = svc.executeBridgeClientManager(sessionID, base, request)
|
||||
case domain.PluginBridgeActionArtifactsOpen:
|
||||
base = svc.executeBridgeArtifactOpen(sessionID, base, request)
|
||||
case domain.PluginBridgeActionAIInvoke:
|
||||
@@ -815,6 +842,146 @@ 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 {
|
||||
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) {
|
||||
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"},
|
||||
}
|
||||
created, err := svc.CreateJob(job)
|
||||
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,
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeRunDistribution(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
|
||||
distribution, err := svc.GenerateRunDistributionForSession(sessionID, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
TargetOS: defaultBridgeValue(request.Payload["targetOs"], "linux"),
|
||||
TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"),
|
||||
IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID),
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "ok"
|
||||
base.Result = map[string]string{
|
||||
"distributionId": distribution.ID,
|
||||
"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),
|
||||
})
|
||||
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),
|
||||
}
|
||||
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")
|
||||
capability := domain.JobCapabilityDependenciesCheck
|
||||
message := "dependency check queued"
|
||||
if action == "install" {
|
||||
capability = domain.JobCapabilityDependenciesInstall
|
||||
message = "dependency install queued"
|
||||
}
|
||||
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),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: capability,
|
||||
TargetKey: defaultBridgeValue(payload["probeKey"], "dependencies/default"),
|
||||
InputRef: payload["inputRef"],
|
||||
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
|
||||
Progress: domain.JobProgress{Percent: 0, Message: message},
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "queued"
|
||||
base.Result = map[string]string{"jobId": job.ID, "state": string(job.State), "capability": job.Capability, "targetKey": job.TargetKey}
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeLogsBackfillRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
if !containsString(plugin.RequiredRunCapabilities, domain.JobCapabilityLogsBackfill) {
|
||||
base.Status = "denied"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "log backfill capability is not declared by plugin"}
|
||||
return base
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: jobIDFromParts("job-logs-backfill", base.RequestID, payload["sourceKey"]),
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
Capability: domain.JobCapabilityLogsBackfill,
|
||||
TargetKey: defaultBridgeValue(payload["sourceKey"], "logs/default"),
|
||||
InputRef: payload["checkpointRef"],
|
||||
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "historical log backfill queued"},
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
base.Status = "queued"
|
||||
base.Result = map[string]string{"jobId": job.ID, "state": string(job.State), "capability": job.Capability, "sourceKey": job.TargetKey}
|
||||
return base
|
||||
}
|
||||
|
||||
func bridgeExecutionError(base domain.PluginBridgeExecuteResponse, err error) domain.PluginBridgeExecuteResponse {
|
||||
base.Status = "error"
|
||||
base.Error = &domain.PluginBridgeSafeError{Code: "execution_failed", Message: safeBridgeReason(err.Error())}
|
||||
@@ -855,6 +1022,11 @@ func pluginPermissionsFromManifest(permissions []string) domain.PluginPermission
|
||||
aggregate.Jobs = true
|
||||
case "server.artifacts.read", "server.artifacts.write":
|
||||
aggregate.Artifacts = true
|
||||
case "server.remote.access":
|
||||
aggregate.RemoteAccess = true
|
||||
case "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage":
|
||||
aggregate.Jobs = true
|
||||
aggregate.Artifacts = true
|
||||
}
|
||||
}
|
||||
return aggregate
|
||||
@@ -962,6 +1134,7 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
|
||||
Pages: plugin.Pages,
|
||||
Tags: plugin.Tags,
|
||||
AIPurposes: plugin.AIPurposes,
|
||||
RemoteAccess: plugin.RemoteAccess,
|
||||
ValidationViolations: plugin.ValidationViolations,
|
||||
Status: plugin.Status,
|
||||
Source: "platform-registry",
|
||||
@@ -1077,6 +1250,37 @@ func (svc *CoreService) GetServerInstanceForSession(sessionID string, id string)
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) UpdateServerInstanceForSession(sessionID string, id string, update domain.ServerInstanceUpdate) (domain.ServerInstance, error) {
|
||||
if err := validator.ValidateServerInstanceUpdate(update); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(id)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
|
||||
return domain.ServerInstance{}, ErrForbidden
|
||||
}
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return domain.ServerInstance{}, validationError("deleted server instances cannot be edited")
|
||||
}
|
||||
if update.Name != nil {
|
||||
instance.Name = *update.Name
|
||||
}
|
||||
instance.UpdatedAt = svc.now()
|
||||
if err := validator.ValidateStoredServerInstance(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListServerInstances(filter domain.ServerInstanceFilter) ([]domain.ServerInstance, error) {
|
||||
return svc.store.ServerInstances().List(filter)
|
||||
}
|
||||
@@ -1487,6 +1691,35 @@ func (svc *CoreService) RemoveServerAdministrator(sessionID string, serverInstan
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ArchiveServerInstanceForSession(sessionID string, serverInstanceID string) (domain.ServerInstance, error) {
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
|
||||
return domain.ServerInstance{}, ErrForbidden
|
||||
}
|
||||
if instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateInstalling {
|
||||
return domain.ServerInstance{}, validationError("running or installing server instances must be stopped before archive")
|
||||
}
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
instance.State = domain.ServerInstanceStateDeleted
|
||||
instance.UpdatedAt = svc.now()
|
||||
if err := validator.ValidateStoredServerInstance(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
return domain.ServerInstance{}, err
|
||||
}
|
||||
return domain.CopyServerInstance(instance), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
|
||||
if job.State == "" {
|
||||
job.State = domain.JobStateQueued
|
||||
@@ -1522,7 +1755,11 @@ func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
|
||||
if err != nil {
|
||||
return domain.Job{}, fmt.Errorf("get server instance dependency: %w", err)
|
||||
}
|
||||
if err := validateJobServerTarget(job, instance); err != nil {
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.Job{}, fmt.Errorf("get server plugin dependency: %w", err)
|
||||
}
|
||||
if err := validateJobServerTarget(job, instance, plugin); err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
}
|
||||
@@ -1632,13 +1869,19 @@ func validateRunnableEndpoint(endpoint domain.RunEndpoint, capability string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateJobServerTarget(job domain.Job, instance domain.ServerInstance) error {
|
||||
func validateJobServerTarget(job domain.Job, instance domain.ServerInstance, plugin domain.GamePlugin) error {
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
return validationError("server instance must not be deleted")
|
||||
}
|
||||
if instance.RunEndpointID != job.RunEndpointID {
|
||||
return validationError("job runEndpointId must match server instance")
|
||||
}
|
||||
if plugin.ID != instance.PluginID {
|
||||
return validationError("job plugin must match server instance")
|
||||
}
|
||||
if !containsString(plugin.RequiredRunCapabilities, job.Capability) {
|
||||
return validationError("plugin missing required capability: " + job.Capability)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user