323 lines
14 KiB
Go
323 lines
14 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"browser.local/run/protocol"
|
|
)
|
|
|
|
const dataTargetSnapshotManifestVersion = 1
|
|
|
|
type dataTargetSnapshotManifest struct {
|
|
Version int `json:"version"`
|
|
DataTargetKey string `json:"dataTargetKey"`
|
|
Kind string `json:"kind"`
|
|
WorkspaceKey string `json:"workspaceKey"`
|
|
SourceRootKey string `json:"sourceRootKey"`
|
|
SourcePathFingerprint string `json:"sourcePathFingerprint"`
|
|
SourceSizeBytes int64 `json:"sourceSizeBytes"`
|
|
SourceModUnixNano int64 `json:"sourceModUnixNano"`
|
|
SnapshotSizeBytes int64 `json:"snapshotSizeBytes"`
|
|
SnapshotChecksum string `json:"snapshotChecksum"`
|
|
MaterializedAt time.Time `json:"materializedAt"`
|
|
}
|
|
|
|
type dataTargetMaterializeError struct {
|
|
code string
|
|
retryable bool
|
|
}
|
|
|
|
func (err dataTargetMaterializeError) Error() string { return err.code }
|
|
|
|
func newDataTargetMaterializeError(code string, retryable bool) error {
|
|
return dataTargetMaterializeError{code: code, retryable: retryable}
|
|
}
|
|
|
|
func sqliteProbeFailureForDataTarget(assignment protocol.RunJobAssignment, err error) LifecycleExecutionResult {
|
|
var materializeErr dataTargetMaterializeError
|
|
if errors.As(err, &materializeErr) {
|
|
return sqliteSchemaProbeFailure(assignment, materializeErr.code, materializeErr.retryable)
|
|
}
|
|
return sqliteSchemaProbeFailure(assignment, "data_target_unavailable", true)
|
|
}
|
|
|
|
func (worker *Worker) materializeSQLiteProbeDataTarget(ctx context.Context, assignment protocol.RunJobAssignment) (string, error) {
|
|
plan, _, ok, err := LoadAutonomousLifecyclePlan(worker.cfg)
|
|
if err != nil {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_plan_invalid", false)
|
|
}
|
|
if !ok || plan == nil || len(plan.DataTargets) == 0 {
|
|
return assignment.TargetKey, nil
|
|
}
|
|
if plan.ServerInstanceID != assignment.ServerInstanceID || plan.RunEndpointID != assignment.RunEndpointID || plan.PluginID != assignment.ExecutionInput.SQLiteSchemaProbe.Binding.PluginID {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_scope_mismatch", false)
|
|
}
|
|
if plan.ProfileKey != "" && assignment.ExecutionInput.WorkspaceScope != "" && plan.ProfileKey != assignment.ExecutionInput.WorkspaceScope {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_scope_mismatch", false)
|
|
}
|
|
var matched *protocol.RunAutonomousDataTarget
|
|
for i := range plan.DataTargets {
|
|
target := &plan.DataTargets[i]
|
|
if dataTargetMatchesSQLiteProbeAssignment(*target, assignment.TargetKey) {
|
|
matched = target
|
|
break
|
|
}
|
|
}
|
|
if matched == nil {
|
|
return assignment.TargetKey, nil
|
|
}
|
|
if !dataTargetSupportsPlatform(matched.Platforms, runtime.GOOS) {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_platform_unsupported", false)
|
|
}
|
|
manifest, err := materializeSQLiteSnapshotDataTarget(ctx, worker.cfg.WorkspaceRoot, assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope, *matched, plan.RuntimeBindings)
|
|
if err != nil {
|
|
log.Printf("RUN phase=data_target.snapshot status=failed target=%s code=%s", safeOptional(matched.Key), safeOptional(dataTargetErrorCode(err)))
|
|
return assignment.TargetKey, err
|
|
}
|
|
log.Printf("RUN phase=data_target.snapshot status=complete target=%s workspaceKey=%s bytes=%d checksum=%s", safeOptional(matched.Key), safeOptional(matched.WorkspaceKey), manifest.SnapshotSizeBytes, safeOptional(manifest.SnapshotChecksum))
|
|
return matched.WorkspaceKey, nil
|
|
}
|
|
|
|
func (worker *Worker) materializeSQLiteQueryDataTarget(ctx context.Context, assignment protocol.RunJobAssignment) (string, error) {
|
|
plan, _, ok, err := LoadAutonomousLifecyclePlan(worker.cfg)
|
|
if err != nil {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_plan_invalid", false)
|
|
}
|
|
if !ok || plan == nil || len(plan.DataTargets) == 0 {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_not_declared", false)
|
|
}
|
|
if plan.ServerInstanceID != assignment.ServerInstanceID || plan.RunEndpointID != assignment.RunEndpointID || plan.PluginID != assignment.ExecutionInput.PluginID {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_scope_mismatch", false)
|
|
}
|
|
if plan.ProfileKey != "" && assignment.ExecutionInput.WorkspaceScope != "" && plan.ProfileKey != assignment.ExecutionInput.WorkspaceScope {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_scope_mismatch", false)
|
|
}
|
|
var matched *protocol.RunAutonomousDataTarget
|
|
for i := range plan.DataTargets {
|
|
target := &plan.DataTargets[i]
|
|
if dataTargetMatchesSQLiteProbeAssignment(*target, assignment.TargetKey) {
|
|
matched = target
|
|
break
|
|
}
|
|
}
|
|
if matched == nil {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_not_declared", false)
|
|
}
|
|
if !dataTargetSupportsPlatform(matched.Platforms, runtime.GOOS) {
|
|
return assignment.TargetKey, newDataTargetMaterializeError("data_target_platform_unsupported", false)
|
|
}
|
|
manifest, err := materializeSQLiteSnapshotDataTarget(ctx, worker.cfg.WorkspaceRoot, assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope, *matched, plan.RuntimeBindings)
|
|
if err != nil {
|
|
log.Printf("RUN phase=data_target.snapshot status=failed target=%s code=%s", safeOptional(matched.Key), safeOptional(dataTargetErrorCode(err)))
|
|
return assignment.TargetKey, err
|
|
}
|
|
log.Printf("RUN phase=data_target.snapshot status=complete target=%s workspaceKey=%s bytes=%d checksum=%s", safeOptional(matched.Key), safeOptional(matched.WorkspaceKey), manifest.SnapshotSizeBytes, safeOptional(manifest.SnapshotChecksum))
|
|
return matched.WorkspaceKey, nil
|
|
}
|
|
|
|
func sqliteQueryFailureForDataTarget(err error) LifecycleExecutionResult {
|
|
var materializeErr dataTargetMaterializeError
|
|
if errors.As(err, &materializeErr) {
|
|
return lifecycleExecutionFailure(materializeErr.code, "SQLite query data target is unavailable", materializeErr.retryable)
|
|
}
|
|
return lifecycleExecutionFailure("data_target_unavailable", "SQLite query data target is unavailable", true)
|
|
}
|
|
|
|
func dataTargetMatchesSQLiteProbeAssignment(target protocol.RunAutonomousDataTarget, assignmentTargetKey string) bool {
|
|
if target.Kind != "sqlite.snapshot" || assignmentTargetKey == "" {
|
|
return false
|
|
}
|
|
return target.Key == assignmentTargetKey || target.TransportKey == assignmentTargetKey || target.WorkspaceKey == assignmentTargetKey
|
|
}
|
|
|
|
func materializeSQLiteSnapshotDataTarget(ctx context.Context, workspaceRoot string, serverInstanceID string, profileKey string, target protocol.RunAutonomousDataTarget, bindings map[string]string) (dataTargetSnapshotManifest, error) {
|
|
if target.Kind != "sqlite.snapshot" || target.RefreshPolicy != "on-demand-snapshot" || target.MaxBytes <= 0 {
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_invalid", false)
|
|
}
|
|
sourceRoot := strings.TrimSpace(bindings[target.SourceRootKey])
|
|
if sourceRoot == "" {
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_source_unbound", false)
|
|
}
|
|
source, sourceInfo, err := resolveDataTargetSource(sourceRoot, target.SourcePath, target.MaxBytes)
|
|
if err != nil {
|
|
return dataTargetSnapshotManifest{}, err
|
|
}
|
|
scope, err := NewWorkspaceResolver(workspaceRoot).Scope(serverInstanceID, profileKey)
|
|
if err != nil {
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_scope_invalid", false)
|
|
}
|
|
destination, _, err := NewWorkspaceResolver(workspaceRoot).WritableTarget(scope, target.WorkspaceKey)
|
|
if err != nil {
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_workspace_invalid", false)
|
|
}
|
|
if err := ensureDirectory(filepath.Dir(destination)); err != nil {
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_workspace_unavailable", true)
|
|
}
|
|
temporary := filepath.Join(filepath.Dir(destination), "."+safeWorkspaceName(filepath.Base(target.WorkspaceKey))+".snapshot.tmp")
|
|
_ = os.Remove(temporary)
|
|
if err := snapshotSQLiteDatabase(ctx, source, temporary, target.MaxBytes); err != nil {
|
|
_ = os.Remove(temporary)
|
|
return dataTargetSnapshotManifest{}, err
|
|
}
|
|
snapshotInfo, err := os.Stat(temporary)
|
|
if err != nil || !snapshotInfo.Mode().IsRegular() || snapshotInfo.Size() <= 0 || snapshotInfo.Size() > target.MaxBytes {
|
|
_ = os.Remove(temporary)
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_snapshot_invalid", true)
|
|
}
|
|
checksum, err := fingerprintSQLiteSource(temporary)
|
|
if err != nil {
|
|
_ = os.Remove(temporary)
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_checksum_failed", true)
|
|
}
|
|
if err := replaceRegularFile(temporary, destination); err != nil {
|
|
_ = os.Remove(temporary)
|
|
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_workspace_unavailable", true)
|
|
}
|
|
manifest := dataTargetSnapshotManifest{
|
|
Version: dataTargetSnapshotManifestVersion,
|
|
DataTargetKey: target.Key,
|
|
Kind: target.Kind,
|
|
WorkspaceKey: target.WorkspaceKey,
|
|
SourceRootKey: target.SourceRootKey,
|
|
SourcePathFingerprint: digestValue(target.SourcePath),
|
|
SourceSizeBytes: sourceInfo.Size(),
|
|
SourceModUnixNano: sourceInfo.ModTime().UnixNano(),
|
|
SnapshotSizeBytes: snapshotInfo.Size(),
|
|
SnapshotChecksum: checksum,
|
|
MaterializedAt: time.Now().UTC(),
|
|
}
|
|
if err := writeDataTargetSnapshotManifest(workspaceRoot, scope, target.WorkspaceKey, manifest); err != nil {
|
|
return dataTargetSnapshotManifest{}, err
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
func resolveDataTargetSource(sourceRoot string, sourcePath string, maxBytes int64) (string, os.FileInfo, error) {
|
|
if strings.TrimSpace(sourceRoot) == "" || strings.TrimSpace(sourcePath) == "" || filepath.IsAbs(sourcePath) || strings.Contains(sourcePath, `\`) || strings.Contains(sourcePath, "..") || !protocol.ValidLogicalFileKey(sourcePath) {
|
|
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
|
|
}
|
|
cleanRoot := filepath.Clean(sourceRoot)
|
|
if !filepath.IsAbs(cleanRoot) {
|
|
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
|
|
}
|
|
source := filepath.Join(cleanRoot, filepath.FromSlash(sourcePath))
|
|
rel, err := filepath.Rel(cleanRoot, source)
|
|
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
|
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
|
|
}
|
|
info, err := os.Lstat(source)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return "", nil, newDataTargetMaterializeError("data_target_source_missing", true)
|
|
}
|
|
return "", nil, newDataTargetMaterializeError("data_target_source_unavailable", true)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
|
|
}
|
|
if info.Size() <= 0 || info.Size() > maxBytes {
|
|
return "", nil, newDataTargetMaterializeError("data_target_source_size_invalid", false)
|
|
}
|
|
return source, info, nil
|
|
}
|
|
|
|
func snapshotSQLiteDatabase(ctx context.Context, source string, destination string, maxBytes int64) error {
|
|
snapshotCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
database, err := sql.Open(sqliteSchemaProbeDriver, "file:"+source+"?mode=ro")
|
|
if err != nil {
|
|
return newDataTargetMaterializeError("data_target_sqlite_open_failed", true)
|
|
}
|
|
defer database.Close()
|
|
database.SetMaxOpenConns(1)
|
|
if _, err := database.ExecContext(snapshotCtx, "VACUUM INTO "+quoteSQLiteStringLiteral(destination)); err != nil {
|
|
return dataTargetSQLiteError(snapshotCtx, err)
|
|
}
|
|
info, err := os.Stat(destination)
|
|
if err != nil {
|
|
return newDataTargetMaterializeError("data_target_snapshot_unavailable", true)
|
|
}
|
|
if info.Size() <= 0 || info.Size() > maxBytes {
|
|
return newDataTargetMaterializeError("data_target_snapshot_limit_exceeded", false)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dataTargetSQLiteError(ctx context.Context, err error) error {
|
|
if errors.Is(ctx.Err(), context.Canceled) {
|
|
return newDataTargetMaterializeError("data_target_cancelled", true)
|
|
}
|
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
return newDataTargetMaterializeError("data_target_timeout", true)
|
|
}
|
|
lower := strings.ToLower(fmt.Sprint(err))
|
|
if strings.Contains(lower, "locked") || strings.Contains(lower, "busy") {
|
|
return newDataTargetMaterializeError("data_target_busy", true)
|
|
}
|
|
return newDataTargetMaterializeError("data_target_snapshot_failed", true)
|
|
}
|
|
|
|
func quoteSQLiteStringLiteral(value string) string {
|
|
return `'` + strings.ReplaceAll(value, `'`, `''`) + `'`
|
|
}
|
|
|
|
func replaceRegularFile(source string, destination string) error {
|
|
if info, err := os.Lstat(destination); err == nil {
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return fmt.Errorf("destination is not a regular file")
|
|
}
|
|
if err := os.Remove(destination); err != nil {
|
|
return err
|
|
}
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
return os.Rename(source, destination)
|
|
}
|
|
|
|
func writeDataTargetSnapshotManifest(workspaceRoot string, scope string, workspaceKey string, manifest dataTargetSnapshotManifest) error {
|
|
manifestKey := workspaceKey + ".snapshot.json"
|
|
path, _, err := NewWorkspaceResolver(workspaceRoot).WritableTarget(scope, manifestKey)
|
|
if err != nil {
|
|
return newDataTargetMaterializeError("data_target_manifest_invalid", false)
|
|
}
|
|
body, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return newDataTargetMaterializeError("data_target_manifest_invalid", false)
|
|
}
|
|
if err := os.WriteFile(path, body, 0o600); err != nil {
|
|
return newDataTargetMaterializeError("data_target_manifest_failed", true)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dataTargetSupportsPlatform(platforms []string, targetOS string) bool {
|
|
if len(platforms) == 0 {
|
|
return true
|
|
}
|
|
for _, platform := range platforms {
|
|
if platform == targetOS {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func dataTargetErrorCode(err error) string {
|
|
var materializeErr dataTargetMaterializeError
|
|
if errors.As(err, &materializeErr) {
|
|
return materializeErr.code
|
|
}
|
|
return "data_target_unavailable"
|
|
}
|