Files
browser/platform/service/scum_live_data.go
T

202 lines
9.1 KiB
Go

package service
import (
"errors"
"strings"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
const (
scumSchemaProbeExecutionKind = "sqlite.schema-probe"
scumSQLiteTemplateExecutionKind = "sqlite.template-query"
scumRCONTemplateExecutionKind = "rcon.template-command"
)
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
idempotencyKey = strings.TrimSpace(idempotencyKey)
if idempotencyKey == "" {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("idempotencyKey is required")
}
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
probe := plugin.SCUMLiveData.Probe
if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin")
}
if !scumSchemaProbeHasDataTarget(plugin.RuntimeProfiles, probe.TargetKey) {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe target is not declared as a generated Run data target")
}
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run is not online for SCUM schema probe")
}
if !containsString(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run does not expose the generic SQLite schema-probe executor")
}
binding, err := svc.runtimeBindingForServer(instance.ID)
if errors.Is(err, repo.ErrNotFound) {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("runtime binding is required before SCUM schema probe")
}
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
if adapterVersion == "" {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema-probe adapter version is not declared")
}
bounds := probe.Bounds
if bounds.MaxObjects == 0 {
bounds = domain.DefaultSCUMSchemaProbeBounds()
}
jobID := jobIDFromParts("job-remote-adapter", instance.ID, idempotencyKey)
request := domain.SCUMSchemaProbeRequest{
RequestID: jobID,
JobID: jobID,
Binding: domain.SCUMBindingIdentity{
ServerInstanceID: instance.ID,
RunBindingID: binding.ID,
RunEndpointID: instance.RunEndpointID,
PluginID: plugin.ID,
PluginVersion: plugin.Version,
AdapterVersion: adapterVersion,
DatabaseIdentity: scumSchemaProbeDatabaseIdentity(probe.TargetKey),
},
Bounds: bounds,
RequestedAt: svc.now(),
}
if err := validator.ValidateSCUMSchemaProbeRequest(request); err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{
ServerInstanceID: instance.ID,
DeclarationKey: probe.TargetKey,
TargetKey: probe.TargetKey,
Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe,
TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds),
MaxAttempts: 1,
IdempotencyKey: idempotencyKey,
PlatformScheduled: true,
SQLiteSchemaProbe: &request,
})
if err != nil {
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
}
return request, result, nil
}
func scumSchemaProbeDatabaseIdentity(targetKey string) string {
trimmed := strings.TrimSpace(targetKey)
return strings.TrimPrefix(trimmed, "databases/")
}
func scumSchemaProbeHasDataTarget(profiles domain.GamePluginRuntimeProfiles, targetKey string) bool {
expectedWorkspaceKey := "databases/" + scumSchemaProbeDatabaseIdentity(targetKey)
for _, target := range profiles.DataTargets {
if target.Key == targetKey && target.Kind == "sqlite.snapshot" && target.WorkspaceKey == expectedWorkspaceKey && target.RefreshPolicy == "on-demand-snapshot" {
return true
}
}
return false
}
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
for _, gate := range manifest.CapabilityGates {
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
return strings.TrimSpace(gate.AdapterVersion)
}
}
for _, gate := range manifest.CapabilityGates {
if strings.TrimSpace(gate.AdapterVersion) != "" {
return strings.TrimSpace(gate.AdapterVersion)
}
}
return ""
}
func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int {
if bounds.TimeoutMS <= 0 {
return 1
}
seconds := (bounds.TimeoutMS + 999) / 1000
if seconds <= 0 {
return 1
}
return seconds
}
func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error {
if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil {
return err
}
expected := job.ExecutionInput.SQLiteSchemaProbe
if expected == nil {
return validationError("SQLite schema probe request is missing from leased job")
}
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
return validationError("SQLite schema probe result does not match leased job identity")
}
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
return validationError("SQLite schema probe result does not match leased binding identity")
}
return nil
}
func validateSCUMSQLiteTemplateResultForJob(job domain.Job, result domain.SCUMSQLiteTemplateResult) error {
if err := validator.ValidateSCUMSQLiteTemplateResult(result); err != nil {
return err
}
expected := job.ExecutionInput.SQLiteTemplate
if expected == nil {
return validationError("SQLite template request is missing from leased job")
}
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
return validationError("SQLite template result does not match leased job identity")
}
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
return validationError("SQLite template result does not match leased binding identity")
}
if result.Capability != expected.Capability || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.SchemaFingerprint != expected.RequiredSchemaFingerprint || result.AssetDigest != expected.AssetDigest || result.ParameterDigest != expected.ParameterDigest {
return validationError("SQLite template result does not match leased template, adapter, digest, or parameter identity")
}
return nil
}
func validateSCUMTypedRCONTemplateResultForJob(job domain.Job, result domain.SCUMTypedRCONTemplateResult) error {
if err := validator.ValidateSCUMTypedRCONTemplateResult(result); err != nil {
return err
}
expected := job.ExecutionInput.RCONTemplate
if expected == nil {
return validationError("typed RCON template request is missing from leased job")
}
if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID {
return validationError("typed RCON template result does not match leased job identity")
}
if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) {
return validationError("typed RCON template result does not match leased binding identity")
}
if result.Capability != expected.Capability || result.TransportKey != expected.TransportKey || result.TargetKey != expected.TargetKey || result.TemplateKey != expected.TemplateKey || result.AdapterVersion != expected.AdapterVersion || result.AssetDigest != expected.AssetDigest || result.PayloadDigest != expected.PayloadDigest || result.ConfirmationDigest != expected.ConfirmationDigest || result.TargetIdentityDigest != expected.TargetIdentityDigest {
return validationError("typed RCON template result does not match leased template, target, digest, or payload identity")
}
if expected.RequiredSchemaFingerprint != "" && result.SchemaFingerprint != expected.RequiredSchemaFingerprint {
return validationError("typed RCON template result does not match leased schema fingerprint")
}
return nil
}
func sameSCUMSchemaProbeBinding(a, b domain.SCUMBindingIdentity) bool {
return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity
}