Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+181 -3
View File
@@ -145,6 +145,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
violations = append(violations, validatePluginPages(plugin.Pages)...)
violations = append(violations, duplicateViolations("tags", plugin.Tags)...)
violations = append(violations, validateAIPurposes(plugin.AIPurposes)...)
violations = append(violations, validateRemoteAccess("remoteAccess", plugin.RemoteAccess, plugin.RequiredRunCapabilities)...)
violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
return finish(violations)
}
@@ -196,6 +197,7 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
violations = append(violations, validatePluginPages(manifest.Pages)...)
violations = append(violations, duplicateViolations("manifest.tags", manifest.Tags)...)
violations = append(violations, validateAIPurposes(manifest.AI.Purposes)...)
violations = append(violations, validateRemoteAccess("manifest.remoteAccess", manifest.RemoteAccess, manifest.Capabilities)...)
violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...)
return finish(violations)
}
@@ -348,6 +350,7 @@ func validatePluginMarketplacePlugin(prefix string, plugin domain.PluginMarketpl
violations = append(violations, validatePluginPages(plugin.Pages)...)
violations = append(violations, duplicateViolations(prefix+".tags", plugin.Tags)...)
violations = append(violations, validateAIPurposes(plugin.AIPurposes)...)
violations = append(violations, validateRemoteAccess(prefix+".remoteAccess", plugin.RemoteAccess, plugin.Capabilities)...)
violations = append(violations, validateSafePluginStrings(prefix, marketplacePluginSafeStrings(plugin))...)
return violations
}
@@ -401,6 +404,14 @@ func AuthorizePluginBridgeAction(plugin domain.GamePlugin, request domain.Plugin
}
func ValidateServerInstance(instance domain.ServerInstance) error {
return validateServerInstance(instance, false)
}
func ValidateStoredServerInstance(instance domain.ServerInstance) error {
return validateServerInstance(instance, true)
}
func validateServerInstance(instance domain.ServerInstance, allowDeleted bool) error {
var violations []string
violations = appendRequired(violations, "id", instance.ID)
violations = appendRequired(violations, "pluginId", instance.PluginID)
@@ -425,7 +436,7 @@ func ValidateServerInstance(instance domain.ServerInstance) error {
if !validServerInstanceState(instance.State) {
violations = append(violations, "state is invalid")
}
if instance.State == domain.ServerInstanceStateDeleted {
if instance.State == domain.ServerInstanceStateDeleted && !allowDeleted {
violations = append(violations, "state must not be deleted on create")
}
if instance.ConfigVersion < 0 {
@@ -434,6 +445,20 @@ func ValidateServerInstance(instance domain.ServerInstance) error {
return finish(violations)
}
func ValidateServerInstanceUpdate(update domain.ServerInstanceUpdate) error {
var violations []string
if update.Name != nil {
name := strings.TrimSpace(*update.Name)
if name == "" {
violations = append(violations, "name is required")
}
if name != *update.Name {
violations = append(violations, "name must not have surrounding whitespace")
}
}
return finish(violations)
}
func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) error {
var violations []string
if plugin.ID == "" {
@@ -698,6 +723,17 @@ func ValidateJob(job domain.Job) error {
violations = append(violations, "inputRef is required for scoped write jobs")
}
}
if isRemoteRunCapability(job.Capability) {
if job.ServerInstanceID == "" {
violations = append(violations, "serverInstanceId is required for remote access jobs")
}
if remoteCapabilityRequiresTargetKey(job.Capability) && job.TargetKey == "" {
violations = append(violations, "targetKey is required for remote access jobs")
}
if remoteCapabilityRequiresInputRef(job.Capability) && job.InputRef == "" {
violations = append(violations, "inputRef is required for remote access jobs")
}
}
return finish(violations)
}
@@ -877,6 +913,63 @@ func validateAIPurposes(purposes []string) []string {
return violations
}
func validateRemoteAccess(field string, remote domain.GamePluginRemoteAccess, declaredCapabilities []string) []string {
var violations []string
if len(remote.Methods) == 0 && len(remote.RunCapabilities) == 0 && len(remote.DatabaseEngines) == 0 && !remote.RCON && !remote.LogTransfer {
return violations
}
if len(remote.Methods) == 0 {
violations = append(violations, field+".methods must not be empty when remote access is declared")
}
for i, method := range remote.Methods {
if !validRemoteAccessMethod(method) {
violations = append(violations, fmt.Sprintf("%s.methods[%d] is not allowed", field, i))
}
}
violations = append(violations, duplicateViolations(field+".methods", remote.Methods)...)
for i, capability := range remote.RunCapabilities {
if !validPluginRunCapability(capability) || !isRemoteRunCapability(capability) {
violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] is not allowed", field, i))
continue
}
if !containsString(declaredCapabilities, capability) {
violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] must also be declared in capabilities", field, i))
}
}
violations = append(violations, duplicateViolations(field+".runCapabilities", remote.RunCapabilities)...)
for i, engine := range remote.DatabaseEngines {
if !validRemoteDatabaseEngine(engine) {
violations = append(violations, fmt.Sprintf("%s.databaseEngines[%d] is not allowed", field, i))
}
}
violations = append(violations, duplicateViolations(field+".databaseEngines", remote.DatabaseEngines)...)
if containsString(remote.Methods, "run") && len(remote.RunCapabilities) == 0 {
violations = append(violations, field+".runCapabilities must not be empty when run access is declared")
}
if containsString(remote.Methods, "ftp") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite}) {
violations = append(violations, field+" requires remote.ftp.read or remote.ftp.write when ftp is declared")
}
if containsString(remote.Methods, "rsync") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite}) {
violations = append(violations, field+" requires remote.rsync.read or remote.rsync.write when rsync is declared")
}
if remote.RCON && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) {
violations = append(violations, field+".rcon requires remote.run.rcon.command")
}
if remote.LogTransfer && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunLogsTransfer) {
violations = append(violations, field+".logTransfer requires remote.run.logs.transfer")
}
for _, engine := range remote.DatabaseEngines {
required := domain.JobCapabilityRemoteRunDBMySQLQuery
if engine == "sqlite" {
required = domain.JobCapabilityRemoteRunDBSQLiteQuery
}
if !containsString(remote.RunCapabilities, required) {
violations = append(violations, fmt.Sprintf("%s.databaseEngines requires %s", field, required))
}
}
return violations
}
func validateSafePluginStrings(prefix string, values []fieldString) []string {
var violations []string
for _, value := range values {
@@ -908,6 +1001,9 @@ func pluginSafeStrings(plugin domain.GamePlugin) []fieldString {
values = appendStringSliceFields(values, "tags", plugin.Tags)
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities)
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", plugin.RemoteAccess.DatabaseEngines)
for i, page := range plugin.Pages {
prefix := fmt.Sprintf("pages[%d]", i)
values = append(values,
@@ -944,6 +1040,9 @@ func manifestSafeStrings(registration domain.GamePluginManifestRegistration) []f
values = appendStringSliceFields(values, "capabilities", manifest.Capabilities)
values = appendStringSliceFields(values, "permissions", manifest.Permissions)
values = appendStringSliceFields(values, "ai.purposes", manifest.AI.Purposes)
values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities)
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines)
for i, page := range manifest.Pages {
prefix := fmt.Sprintf("pages[%d]", i)
values = append(values,
@@ -979,6 +1078,9 @@ func marketplacePluginSafeStrings(plugin domain.PluginMarketplacePlugin) []field
values = appendStringSliceFields(values, "tags", plugin.Tags)
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities)
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", plugin.RemoteAccess.DatabaseEngines)
for i, page := range plugin.Pages {
prefix := fmt.Sprintf("pages[%d]", i)
values = append(values,
@@ -1132,7 +1234,14 @@ func validPluginRunCapability(capability string) bool {
"config.write",
"files.list", "files.read", "files.write", "files.patch",
"file.list", "file.read", "file.write", "file.patch",
"logs.read", "log.query",
"logs.read", "log.query", domain.JobCapabilityLogsBackfill,
domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite,
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
"artifacts.read", "artifacts.write", "artifact.read", "artifact.write",
"ai.invoke":
return true
@@ -1141,6 +1250,51 @@ func validPluginRunCapability(capability string) bool {
}
}
func isRemoteRunCapability(capability string) bool {
return strings.HasPrefix(capability, "remote.")
}
func remoteCapabilityRequiresTargetKey(capability string) bool {
switch capability {
case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop:
return false
default:
return isRemoteRunCapability(capability)
}
}
func remoteCapabilityRequiresInputRef(capability string) bool {
switch capability {
case domain.JobCapabilityRemoteFTPWrite,
domain.JobCapabilityRemoteRsyncWrite,
domain.JobCapabilityRemoteRunFilesWrite,
domain.JobCapabilityRemoteRunDBMySQLQuery,
domain.JobCapabilityRemoteRunDBSQLiteQuery,
domain.JobCapabilityRemoteRunRCONCommand:
return true
default:
return false
}
}
func validRemoteAccessMethod(method string) bool {
switch method {
case "ftp", "rsync", "run":
return true
default:
return false
}
}
func validRemoteDatabaseEngine(engine string) bool {
switch engine {
case "mysql", "sqlite":
return true
default:
return false
}
}
func validFileOperationKind(operation domain.FileOperationKind) bool {
switch operation {
case domain.FileOperationRead, domain.FileOperationWrite:
@@ -1186,7 +1340,7 @@ func validScopedInputRef(ref string) bool {
func validPluginPermission(permission string) bool {
switch permission {
case "server.create", "server.read", "server.lifecycle", "server.files.read", "server.files.write", "server.logs.read", "server.artifacts.read", "server.artifacts.write", "ai.invoke":
case "server.create", "server.read", "server.lifecycle", "server.files.read", "server.files.write", "server.logs.read", "server.artifacts.read", "server.artifacts.write", "server.remote.access", "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage", "ai.invoke":
return true
default:
return false
@@ -1200,6 +1354,11 @@ func validPluginBridgeAction(action domain.PluginBridgeAction) bool {
domain.PluginBridgeActionLogsQuery,
domain.PluginBridgeActionArtifactsOpen,
domain.PluginBridgeActionFilesRequest,
domain.PluginBridgeActionRemoteAccessRequest,
domain.PluginBridgeActionRunDistribution,
domain.PluginBridgeActionDependenciesRequest,
domain.PluginBridgeActionLogsBackfillRequest,
domain.PluginBridgeActionClientManager,
domain.PluginBridgeActionAIInvoke:
return true
default:
@@ -1219,6 +1378,16 @@ func requiredBridgePermissions(action domain.PluginBridgeAction) []string {
return []string{"server.artifacts.read"}
case domain.PluginBridgeActionFilesRequest:
return []string{"server.files.read"}
case domain.PluginBridgeActionRemoteAccessRequest:
return []string{"server.remote.access"}
case domain.PluginBridgeActionRunDistribution:
return []string{"server.run.distribution"}
case domain.PluginBridgeActionDependenciesRequest:
return []string{"server.dependencies.manage"}
case domain.PluginBridgeActionLogsBackfillRequest:
return []string{"server.logs.read"}
case domain.PluginBridgeActionClientManager:
return []string{"server.client-manager.manage"}
case domain.PluginBridgeActionAIInvoke:
return []string{"ai.invoke"}
default:
@@ -1259,6 +1428,15 @@ func containsAll(values []string, required []string) bool {
return true
}
func containsAny(values []string, candidates []string) bool {
for _, candidate := range candidates {
if containsString(values, candidate) {
return true
}
}
return false
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {