승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
127 lines
4.9 KiB
Go
127 lines
4.9 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
edgenode "iop/apps/edge/internal/node"
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
var (
|
|
ErrSingleRequestWorkspaceUnavailable = errors.New("single-request workspace: approved workspace is unavailable")
|
|
ErrSingleRequestWorkspaceStale = errors.New("single-request workspace: ready connection changed before executor handoff")
|
|
)
|
|
|
|
// bindSingleRequestWorkspace compiles the caller's already-authorized opaque
|
|
// workspace ref into a request-stable capability projection. It deliberately
|
|
// resolves only the configured catalog owner and then only that exact node id's
|
|
// ready owner; aliases, caller Node/path input, and implicit registry selection
|
|
// are not admission paths.
|
|
func bindSingleRequestWorkspace(binding *SingleRequestBinding, store *edgenode.NodeStore, registry *edgenode.Registry) (*SingleRequestBinding, error) {
|
|
base, err := cloneValidatedSingleRequestBinding(binding)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrSingleRequestInvalidBinding, err)
|
|
}
|
|
if store == nil || registry == nil {
|
|
return nil, ErrSingleRequestWorkspaceUnavailable
|
|
}
|
|
|
|
owner, workspace, err := store.ResolveWorkspace(base.WorkspaceRef)
|
|
if err != nil || owner == nil || owner.ID == "" || workspace.Ref != base.WorkspaceRef {
|
|
return nil, fmt.Errorf("%w: workspace ref %q", ErrSingleRequestWorkspaceUnavailable, base.WorkspaceRef)
|
|
}
|
|
ready, ok := registry.ReadyOwnerSnapshot(owner.ID)
|
|
if !ok || ready == nil || ready.NodeID != owner.ID || ready.ConnectionGeneration == 0 {
|
|
return nil, fmt.Errorf("%w: node %q is not ready", ErrSingleRequestWorkspaceUnavailable, owner.ID)
|
|
}
|
|
|
|
workspaceBinding, err := compileSingleRequestWorkspaceBinding(workspace, ready)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrSingleRequestWorkspaceUnavailable, err)
|
|
}
|
|
base.Workspace = workspaceBinding
|
|
if err := applySingleRequestWorkspaceEffectiveLimits(base); err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrSingleRequestWorkspaceUnavailable, err)
|
|
}
|
|
return base, nil
|
|
}
|
|
|
|
func compileSingleRequestWorkspaceBinding(workspace config.WorkspaceDefinition, ready *edgenode.NodeEntry) (*SingleRequestWorkspaceBinding, error) {
|
|
if ready == nil || workspace.Ref == "" || ready.NodeID == "" || ready.ConnectionGeneration == 0 {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
operations := make([]string, 0, len(workspace.Operations))
|
|
commandEnabled := false
|
|
for _, operation := range workspace.Operations {
|
|
switch operation {
|
|
case config.WorkspaceOpRead, config.WorkspaceOpList, config.WorkspaceOpWrite, config.WorkspaceOpDelete, config.WorkspaceOpCommand:
|
|
operations = append(operations, string(operation))
|
|
commandEnabled = commandEnabled || operation == config.WorkspaceOpCommand
|
|
default:
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
}
|
|
if len(operations) == 0 || (len(workspace.Commands) > 0 && !commandEnabled) {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
sort.Strings(operations)
|
|
if !sortedUniqueNonEmpty(operations) {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
|
|
commandIDs := make([]string, 0, len(workspace.Commands))
|
|
for _, command := range workspace.Commands {
|
|
if command.ID == "" {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
commandIDs = append(commandIDs, command.ID)
|
|
}
|
|
sort.Strings(commandIDs)
|
|
if !sortedUniqueNonEmpty(commandIDs) {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
environmentNames := append([]string(nil), workspace.EnvironmentAllowlist...)
|
|
sort.Strings(environmentNames)
|
|
if !sortedUniqueNonEmpty(environmentNames) {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
for _, name := range environmentNames {
|
|
if !validEnvironmentName(name) {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
}
|
|
limits := SingleRequestWorkspaceLimits{
|
|
MaxReadBytes: workspace.MaxReadBytes,
|
|
MaxWriteBytes: workspace.MaxWriteBytes,
|
|
MaxOutputBytes: workspace.MaxOutputBytes,
|
|
MaxCommandTimeoutMS: workspace.MaxCommandTimeoutMS,
|
|
}
|
|
if err := validateSingleRequestWorkspaceCapabilities(operations, commandIDs, limits); err != nil {
|
|
return nil, errSingleRequestWorkspaceMalformed
|
|
}
|
|
return &SingleRequestWorkspaceBinding{
|
|
Ref: workspace.Ref,
|
|
NodeID: ready.NodeID,
|
|
ConnectionGeneration: ready.ConnectionGeneration,
|
|
OperationIDs: operations,
|
|
CommandIDs: commandIDs,
|
|
EnvironmentNames: environmentNames,
|
|
Limits: limits,
|
|
}, nil
|
|
}
|
|
|
|
func applySingleRequestWorkspaceEffectiveLimits(binding *SingleRequestBinding) error {
|
|
if binding == nil || binding.Workspace == nil {
|
|
return errSingleRequestWorkspaceMalformed
|
|
}
|
|
if binding.Limits.MaxOutputBytes < binding.Workspace.Limits.MaxOutputBytes {
|
|
binding.Workspace.Limits.MaxOutputBytes = binding.Limits.MaxOutputBytes
|
|
}
|
|
if binding.Limits.StageTimeoutMS < binding.Workspace.Limits.MaxCommandTimeoutMS {
|
|
binding.Workspace.Limits.MaxCommandTimeoutMS = binding.Limits.StageTimeoutMS
|
|
}
|
|
_, err := validateAndCloneWorkspaceBinding(binding.Workspace)
|
|
return err
|
|
}
|