mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-26 11:34:55 +02:00
feat: add worker management, hooks support, and db cleanup enhancements
- Add worker eval command for distributed function execution with Redis hooks registration - Add worker set command to update worker fields (alias, public-ip, ssh-enabled, ssh-keys-path) - Enhance worker status with JSON output, search filtering, and column selection (--columns, --exclude-columns, --search) - Add --keep-setting flag to install base/validate commands to preserve osm-settings.yaml after base installation - Fix binary installation in Nix: replace CopyInstalledBinaryToFolder with SymlinkInstalledBinaryToFolder - Add --clean-ws flag to db clean command for removing workspace data - Add HooksEnabled field to Run records when creating runs from CLI and API - Add comprehensive test coverage for hook execution (pre/post hooks, execution order, failure handling) - Add test coverage for worker commands (eval, set, status with JSON) and db clean operations - Improve usage documentation for worker subcommands and db operations
This commit is contained in:
+91
-44
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -11,8 +12,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/j3ssie/osmedeus/v5/internal/config"
|
||||
"github.com/j3ssie/osmedeus/v5/internal/database"
|
||||
"github.com/j3ssie/osmedeus/v5/internal/distributed"
|
||||
"github.com/j3ssie/osmedeus/v5/internal/executor"
|
||||
"github.com/j3ssie/osmedeus/v5/internal/functions"
|
||||
"github.com/j3ssie/osmedeus/v5/internal/template"
|
||||
@@ -105,6 +108,10 @@ func init() {
|
||||
evalCmd.Flags().BoolVar(&funcRepeat, "repeat", false, "repeat run after completion")
|
||||
evalCmd.Flags().StringVar(&funcRepeatWaitTime, "repeat-wait-time", "5s", "wait time between repeats (e.g., 30s, 20m, 10h, 1d)")
|
||||
|
||||
// Distributed mode flags for eval commands
|
||||
evalCmd.Flags().StringVar(&redisURL, "redis-url", "", "Redis connection URL for distributed mode (enables run_on_master/run_on_worker routing)")
|
||||
functionEvalCmd.Flags().StringVar(&redisURL, "redis-url", "", "Redis connection URL for distributed mode (enables run_on_master/run_on_worker routing)")
|
||||
|
||||
functionCmd.AddCommand(functionEvalCmd)
|
||||
functionCmd.AddCommand(functionListCmd)
|
||||
}
|
||||
@@ -140,56 +147,40 @@ func runFunctionEval(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Determine script source: --function-file > -f flag > positional arg > -e flag > stdin
|
||||
var script string
|
||||
|
||||
// Read script from function file if provided
|
||||
if funcFunctionFile != "" {
|
||||
data, err := os.ReadFile(funcFunctionFile)
|
||||
if err != nil {
|
||||
printer.Error("Failed to read function file: %s", err)
|
||||
return fmt.Errorf("failed to read function file: %w", err)
|
||||
}
|
||||
script = strings.TrimSpace(string(data))
|
||||
} else if evalFunctionName != "" {
|
||||
// Handle -f/--function flag: build script from function name + positional args
|
||||
var quotedArgs []string
|
||||
for _, arg := range args {
|
||||
quotedArgs = append(quotedArgs, fmt.Sprintf("%q", arg))
|
||||
}
|
||||
script = fmt.Sprintf("%s(%s)", evalFunctionName, strings.Join(quotedArgs, ", "))
|
||||
} else if len(args) > 0 && args[0] != "-" {
|
||||
if len(args) > 1 {
|
||||
// Multiple args: treat first as function name, rest as arguments
|
||||
// e.g., "func_name arg1 arg2" → "func_name("arg1", "arg2")"
|
||||
var quotedArgs []string
|
||||
for _, arg := range args[1:] {
|
||||
quotedArgs = append(quotedArgs, fmt.Sprintf("%q", arg))
|
||||
// If --redis-url is provided, register distributed hooks for master-mode eval
|
||||
if redisURL != "" {
|
||||
cfg := config.Get()
|
||||
if cfg != nil {
|
||||
redisCfg, err := distributed.ParseRedisURL(redisURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
script = fmt.Sprintf("%s(%s)", args[0], strings.Join(quotedArgs, ", "))
|
||||
} else {
|
||||
// Single arg: check if it is a bare function name and add () if needed
|
||||
script = normalizeScriptExpression(args[0])
|
||||
cfg.Redis = *redisCfg
|
||||
}
|
||||
} else if evalScript != "" {
|
||||
// Script provided via -e flag
|
||||
script = evalScript
|
||||
} else if evalStdin || (len(args) > 0 && args[0] == "-") {
|
||||
// Read script from stdin
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if cfg == nil || !cfg.IsRedisConfigured() {
|
||||
return fmt.Errorf("redis not configured")
|
||||
}
|
||||
client, err := distributed.NewClientFromConfig(cfg)
|
||||
if err != nil {
|
||||
printer.Error("Failed to read from stdin: %s", err)
|
||||
return fmt.Errorf("failed to read from stdin: %w", err)
|
||||
return fmt.Errorf("failed to create redis client: %w", err)
|
||||
}
|
||||
script = strings.TrimSpace(string(data))
|
||||
defer client.Close()
|
||||
ctx := context.Background()
|
||||
if err := client.Ping(ctx); err != nil {
|
||||
return fmt.Errorf("failed to connect to redis: %w", err)
|
||||
}
|
||||
hostname, _ := os.Hostname()
|
||||
masterID := fmt.Sprintf("%s-master-eval-%s", hostname, uuid.NewString()[:8])
|
||||
config.SetWorkerMode(true, masterID)
|
||||
cleanup := distributed.RegisterDistributedHooksFromClient(client, masterID)
|
||||
defer cleanup()
|
||||
printer.Info("Distributed mode enabled (id: %s)", terminal.Cyan(masterID))
|
||||
}
|
||||
|
||||
if script != "" {
|
||||
script = normalizeScriptExpression(script)
|
||||
}
|
||||
|
||||
if script == "" {
|
||||
return fmt.Errorf("no script provided: use positional argument, -e flag, --function-file, or --stdin")
|
||||
script, err := resolveEvalScript(args)
|
||||
if err != nil {
|
||||
printer.Error("%s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Main execution loop
|
||||
@@ -452,6 +443,62 @@ func runFunctionList(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveEvalScript determines the script to execute from various sources:
|
||||
// --function-file > -f flag > positional arg > -e flag > --stdin.
|
||||
// Returns the resolved script string or an error.
|
||||
func resolveEvalScript(args []string) (string, error) {
|
||||
var script string
|
||||
|
||||
// Read script from function file if provided
|
||||
if funcFunctionFile != "" {
|
||||
data, err := os.ReadFile(funcFunctionFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read function file: %w", err)
|
||||
}
|
||||
script = strings.TrimSpace(string(data))
|
||||
} else if evalFunctionName != "" {
|
||||
// Handle -f/--function flag: build script from function name + positional args
|
||||
var quotedArgs []string
|
||||
for _, arg := range args {
|
||||
quotedArgs = append(quotedArgs, fmt.Sprintf("%q", arg))
|
||||
}
|
||||
script = fmt.Sprintf("%s(%s)", evalFunctionName, strings.Join(quotedArgs, ", "))
|
||||
} else if len(args) > 0 && args[0] != "-" {
|
||||
if len(args) > 1 {
|
||||
// Multiple args: treat first as function name, rest as arguments
|
||||
// e.g., "func_name arg1 arg2" → "func_name("arg1", "arg2")"
|
||||
var quotedArgs []string
|
||||
for _, arg := range args[1:] {
|
||||
quotedArgs = append(quotedArgs, fmt.Sprintf("%q", arg))
|
||||
}
|
||||
script = fmt.Sprintf("%s(%s)", args[0], strings.Join(quotedArgs, ", "))
|
||||
} else {
|
||||
// Single arg: check if it is a bare function name and add () if needed
|
||||
script = normalizeScriptExpression(args[0])
|
||||
}
|
||||
} else if evalScript != "" {
|
||||
// Script provided via -e flag
|
||||
script = evalScript
|
||||
} else if evalStdin || (len(args) > 0 && args[0] == "-") {
|
||||
// Read script from stdin
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
script = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
if script != "" {
|
||||
script = normalizeScriptExpression(script)
|
||||
}
|
||||
|
||||
if script == "" {
|
||||
return "", fmt.Errorf("no script provided: use positional argument, -e flag, --function-file, or --stdin")
|
||||
}
|
||||
|
||||
return script, nil
|
||||
}
|
||||
|
||||
// normalizeScriptExpression checks if the input is a bare function name (without parentheses)
|
||||
// and adds () to make it a valid function call. This allows "osmedeus eval cdn_ls_tree"
|
||||
// to work the same as "osmedeus eval cdn_ls_tree()"
|
||||
|
||||
Reference in New Issue
Block a user