189 lines
8.0 KiB
Go
189 lines
8.0 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"browser.local/run/protocol"
|
|
)
|
|
|
|
type RemoteAdapterRequest struct {
|
|
JobID string
|
|
ServerInstanceID string
|
|
AdapterKey string
|
|
AdapterKind string
|
|
TargetKey string
|
|
Capability string
|
|
InputRef string
|
|
}
|
|
|
|
type RemoteAdapterOutcome struct {
|
|
Message string
|
|
ResultRef string
|
|
Retryable bool
|
|
}
|
|
|
|
type RemoteAdapter interface {
|
|
Execute(context.Context, RemoteAdapterRequest) (RemoteAdapterOutcome, error)
|
|
}
|
|
|
|
type RemoteAdapterFunc func(context.Context, RemoteAdapterRequest) (RemoteAdapterOutcome, error)
|
|
|
|
func (fn RemoteAdapterFunc) Execute(ctx context.Context, request RemoteAdapterRequest) (RemoteAdapterOutcome, error) {
|
|
return fn(ctx, request)
|
|
}
|
|
|
|
type RemoteAdapterRegistry struct {
|
|
mu sync.RWMutex
|
|
adapters map[string]RemoteAdapter
|
|
}
|
|
|
|
func NewRemoteAdapterRegistry() *RemoteAdapterRegistry {
|
|
registry := &RemoteAdapterRegistry{adapters: map[string]RemoteAdapter{}}
|
|
for _, kind := range []string{"ftp", "rsync", "run-file", "run-process", "database", "log-transfer"} {
|
|
registry.adapters[kind] = declaredRemoteAdapter{kind: kind}
|
|
}
|
|
return registry
|
|
}
|
|
|
|
func (registry *RemoteAdapterRegistry) Register(kind string, adapter RemoteAdapter) error {
|
|
kind = strings.TrimSpace(kind)
|
|
if kind == "" || adapter == nil {
|
|
return fmt.Errorf("remote adapter kind and implementation are required")
|
|
}
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
registry.adapters[kind] = adapter
|
|
return nil
|
|
}
|
|
|
|
func (registry *RemoteAdapterRegistry) adapter(kind string) (RemoteAdapter, bool) {
|
|
registry.mu.RLock()
|
|
defer registry.mu.RUnlock()
|
|
adapter, exists := registry.adapters[kind]
|
|
return adapter, exists
|
|
}
|
|
|
|
func ExecuteRemoteAccessJob(ctx context.Context, assignment protocol.RunJobAssignment) LifecycleExecutionResult {
|
|
return ExecuteRemoteAccessJobWithRegistry(ctx, assignment, NewRemoteAdapterRegistry())
|
|
}
|
|
|
|
func ExecuteRemoteAccessJobWithRegistry(ctx context.Context, assignment protocol.RunJobAssignment, registry *RemoteAdapterRegistry) LifecycleExecutionResult {
|
|
if assignment.ExecutionInput.SourceRCON != nil {
|
|
return lifecycleFailure("source_rcon_requires_worker_transport", "Source RCON commands require the one-time worker transport")
|
|
}
|
|
if err := protocol.ValidateRunJobAssignment(assignment); err != nil {
|
|
if strings.Contains(err.Error(), "remoteAdapterKey") {
|
|
return lifecycleFailure("unsafe_remote_adapter_target", "remote adapter key must be an approved logical key")
|
|
}
|
|
return lifecycleFailure("unsafe_remote_access_job", err.Error())
|
|
}
|
|
if !isSupportedRemoteCapability(assignment.Capability) {
|
|
return lifecycleFailure("unsupported_remote_access_capability", "unsupported remote access capability")
|
|
}
|
|
if registry == nil {
|
|
return lifecycleFailure("remote_adapter_unavailable", "remote adapter registry is unavailable")
|
|
}
|
|
adapterKind := strings.TrimSpace(assignment.ExecutionInput.RemoteAdapterKind)
|
|
if adapterKind == "" {
|
|
adapterKind = adapterKindForCapability(assignment.Capability)
|
|
}
|
|
adapterKey := strings.TrimSpace(assignment.ExecutionInput.RemoteAdapterKey)
|
|
if adapterKey == "" {
|
|
adapterKey = assignment.TargetKey
|
|
}
|
|
if !protocol.ValidLogicalFileKey(adapterKey) || !protocol.ValidLogicalFileKey(assignment.TargetKey) {
|
|
return lifecycleFailure("unsafe_remote_adapter_target", "remote adapter and target must use approved logical keys")
|
|
}
|
|
if !adapterKindAllowsCapability(adapterKind, assignment.Capability) {
|
|
return lifecycleFailure("remote_adapter_capability_mismatch", "remote adapter kind does not allow requested capability")
|
|
}
|
|
adapter, exists := registry.adapter(adapterKind)
|
|
if !exists {
|
|
return lifecycleFailure("remote_adapter_unavailable", "declared remote adapter is unavailable")
|
|
}
|
|
|
|
executionCtx := ctx
|
|
cancel := func() {}
|
|
if timeout := assignment.ExecutionInput.TimeoutSeconds; timeout > 0 {
|
|
if timeout > 300 {
|
|
return lifecycleFailure("unsafe_remote_adapter_timeout", "remote adapter timeout exceeds bound")
|
|
}
|
|
executionCtx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
|
}
|
|
defer cancel()
|
|
|
|
request := RemoteAdapterRequest{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, AdapterKey: adapterKey, AdapterKind: adapterKind, TargetKey: assignment.TargetKey, Capability: assignment.Capability, InputRef: assignment.InputRef}
|
|
outcome, err := adapter.Execute(executionCtx, request)
|
|
if err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(executionCtx.Err(), context.DeadlineExceeded) {
|
|
return LifecycleExecutionResult{State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter timed out"}, Message: "remote adapter timed out", ErrorCode: "remote_adapter_timeout", Retryable: true}
|
|
}
|
|
if errors.Is(err, context.Canceled) || errors.Is(executionCtx.Err(), context.Canceled) {
|
|
return LifecycleExecutionResult{State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter cancelled"}, Message: "remote adapter cancelled", ErrorCode: "remote_adapter_cancelled"}
|
|
}
|
|
return LifecycleExecutionResult{State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter failed"}, Message: "remote adapter failed", ErrorCode: "remote_adapter_failed", Retryable: outcome.Retryable}
|
|
}
|
|
if err := executionCtx.Err(); err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return LifecycleExecutionResult{State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter timed out"}, Message: "remote adapter timed out", ErrorCode: "remote_adapter_timeout", Retryable: true}
|
|
}
|
|
return LifecycleExecutionResult{State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter cancelled"}, Message: "remote adapter cancelled", ErrorCode: "remote_adapter_cancelled"}
|
|
}
|
|
resultRef := outcome.ResultRef
|
|
if resultRef == "" {
|
|
resultRef = fmt.Sprintf("artifact://jobs/%s/remote-access-result", url.PathEscape(assignment.JobID))
|
|
}
|
|
message := strings.TrimSpace(outcome.Message)
|
|
if message == "" {
|
|
message = fmt.Sprintf("%s completed through declared %s adapter", assignment.Capability, adapterKind)
|
|
}
|
|
return LifecycleExecutionResult{State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "remote adapter completed"}, ResultRef: resultRef, Message: message}
|
|
}
|
|
|
|
type declaredRemoteAdapter struct {
|
|
kind string
|
|
}
|
|
|
|
func (adapter declaredRemoteAdapter) Execute(ctx context.Context, request RemoteAdapterRequest) (RemoteAdapterOutcome, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return RemoteAdapterOutcome{}, ctx.Err()
|
|
default:
|
|
}
|
|
if request.AdapterKind != adapter.kind || request.ServerInstanceID == "" || request.JobID == "" {
|
|
return RemoteAdapterOutcome{}, fmt.Errorf("remote adapter request identity mismatch")
|
|
}
|
|
return RemoteAdapterOutcome{Message: fmt.Sprintf("%s completed through bounded remote access envelope", request.Capability), ResultRef: fmt.Sprintf("artifact://jobs/%s/remote-access-result", url.PathEscape(request.JobID))}, nil
|
|
}
|
|
|
|
func adapterKindForCapability(capability string) string {
|
|
switch capability {
|
|
case protocol.RunCapabilityRemoteFTPRead, protocol.RunCapabilityRemoteFTPWrite:
|
|
return "ftp"
|
|
case protocol.RunCapabilityRemoteRsyncRead, protocol.RunCapabilityRemoteRsyncWrite:
|
|
return "rsync"
|
|
case protocol.RunCapabilityRemoteRunFilesRead, protocol.RunCapabilityRemoteRunFilesWrite:
|
|
return "run-file"
|
|
case protocol.RunCapabilityRemoteRunProcessStart, protocol.RunCapabilityRemoteRunProcessStop:
|
|
return "run-process"
|
|
case protocol.RunCapabilityRemoteRunDBMySQLQuery, protocol.RunCapabilityRemoteRunDBSQLiteQuery:
|
|
return "database"
|
|
case protocol.RunCapabilityRemoteRunRCONCommand:
|
|
return "rcon"
|
|
case protocol.RunCapabilityRemoteRunLogsTransfer:
|
|
return "log-transfer"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func adapterKindAllowsCapability(kind string, capability string) bool {
|
|
return kind != "" && kind == adapterKindForCapability(capability)
|
|
}
|