From e7eea69a40269d43fc551c3a1d3dd341d92b2b1a Mon Sep 17 00:00:00 2001 From: j3ssie Date: Sat, 24 Jan 2026 16:22:44 +0800 Subject: [PATCH] feat: expose platform variables in osmedeus eval command Export platform detection functions (DetectDocker, DetectKubernetes, DetectCloudProvider) from internal/executor to allow reuse in CLI. Inject PlatformOS, PlatformArch, PlatformInDocker, PlatformInKubernetes, and PlatformCloudProvider variables in func eval command context. --- internal/executor/executor.go | 36 ++++-- internal/executor/platform.go | 79 ++++++++++++ internal/executor/platform_test.go | 201 +++++++++++++++++++++++++++++ pkg/cli/function.go | 10 ++ 4 files changed, 315 insertions(+), 11 deletions(-) create mode 100644 internal/executor/platform.go create mode 100644 internal/executor/platform_test.go diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 82824cf..bb3ca33 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -8,6 +8,7 @@ import ( "math/rand" "os" "path/filepath" + "runtime" "strconv" "strings" "sync" @@ -223,6 +224,13 @@ func (e *Executor) injectBuiltinVariables(cfg *config.Config, params map[string] // Version info execCtx.SetVariable("Version", core.VERSION) + // Platform detection variables + execCtx.SetVariable("PlatformOS", runtime.GOOS) + execCtx.SetVariable("PlatformArch", runtime.GOARCH) + execCtx.SetVariable("PlatformInDocker", DetectDocker()) + execCtx.SetVariable("PlatformInKubernetes", DetectKubernetes()) + execCtx.SetVariable("PlatformCloudProvider", DetectCloudProvider()) + // Target-based variables target := params["target"] targetFileParam := params["target_file"] @@ -373,7 +381,7 @@ func (e *Executor) debugLogTargetVariables(execCtx *core.ExecutionContext) { execCtx.Logger.Debug("Target variables", zap.String("workflow", execCtx.WorkflowName), - zap.String("run_id", execCtx.RunUUID), + zap.String("run_uuid", execCtx.RunUUID), zap.String("Target", getStr("Target")), zap.String("TargetSpace", getStr("TargetSpace")), zap.String("Output", getStr("Output")), @@ -725,13 +733,16 @@ func (e *Executor) ExecuteModule(ctx context.Context, module *core.Workflow, par } // Create execution context - runID := uuid.New().String()[:8] + runUUID := e.dbRunUUID + if runUUID == "" { + runUUID = uuid.New().String() // Full UUID when not in server mode + } e.logger.Debug("Created execution context", - zap.String("run_id", runID), + zap.String("run_uuid", runUUID), zap.String("target", params["target"]), ) - execCtx := core.NewExecutionContext(module.Name, core.KindModule, runID, params["target"]) - execCtx.Logger = logger.WithWorkflow(module.Name, runID) + execCtx := core.NewExecutionContext(module.Name, core.KindModule, runUUID, params["target"]) + execCtx.Logger = logger.WithWorkflow(module.Name, runUUID) // Create and setup runner based on workflow configuration binaryPath, _ := os.Executable() @@ -860,7 +871,7 @@ func (e *Executor) ExecuteModule(ctx context.Context, module *core.Workflow, par result := &core.WorkflowResult{ WorkflowName: module.Name, WorkflowKind: core.KindModule, - RunUUID: runID, + RunUUID: runUUID, Target: params["target"], Status: core.RunStatusRunning, StartTime: time.Now(), @@ -1378,13 +1389,16 @@ func (e *Executor) ExecuteFlow(ctx context.Context, flow *core.Workflow, params } // Create execution context - runID := uuid.New().String()[:8] + runUUID := e.dbRunUUID + if runUUID == "" { + runUUID = uuid.New().String() // Full UUID when not in server mode + } e.logger.Debug("Created flow execution context", - zap.String("run_id", runID), + zap.String("run_uuid", runUUID), zap.String("target", params["target"]), ) - execCtx := core.NewExecutionContext(flow.Name, core.KindFlow, runID, params["target"]) - execCtx.Logger = logger.WithWorkflow(flow.Name, runID) + execCtx := core.NewExecutionContext(flow.Name, core.KindFlow, runUUID, params["target"]) + execCtx.Logger = logger.WithWorkflow(flow.Name, runUUID) // Inject builtin variables e.logger.Debug("Injecting builtin variables for flow") @@ -1455,7 +1469,7 @@ func (e *Executor) ExecuteFlow(ctx context.Context, flow *core.Workflow, params result := &core.WorkflowResult{ WorkflowName: flow.Name, WorkflowKind: core.KindFlow, - RunUUID: runID, + RunUUID: runUUID, Target: params["target"], Status: core.RunStatusRunning, StartTime: time.Now(), diff --git a/internal/executor/platform.go b/internal/executor/platform.go new file mode 100644 index 0000000..d68000d --- /dev/null +++ b/internal/executor/platform.go @@ -0,0 +1,79 @@ +package executor + +import ( + "os" + "runtime" + "strings" +) + +// DetectDocker checks if running inside a Docker container +func DetectDocker() bool { + // Method 1: Check for /.dockerenv file + if _, err := os.Stat("/.dockerenv"); err == nil { + return true + } + + // Method 2: Check /proc/1/cgroup for /docker/ (Linux only) + if runtime.GOOS == "linux" { + data, err := os.ReadFile("/proc/1/cgroup") + if err == nil && strings.Contains(string(data), "/docker/") { + return true + } + } + + return false +} + +// DetectKubernetes checks if running inside a Kubernetes pod +func DetectKubernetes() bool { + // Method 1: Check for Kubernetes service account directory + if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount"); err == nil { + return true + } + + // Method 2: Check /proc/1/cgroup for kubepods (Linux only) + if runtime.GOOS == "linux" { + data, err := os.ReadFile("/proc/1/cgroup") + if err == nil { + content := string(data) + if strings.Contains(content, "/kubepods/") || strings.Contains(content, "/kubelet/") { + return true + } + } + } + + return false +} + +// DetectCloudProvider detects AWS, GCP, Azure, or returns "local" +func DetectCloudProvider() string { + // Only works on Linux - check DMI information + if runtime.GOOS != "linux" { + return "local" + } + + // Check sys_vendor + vendorPaths := []string{ + "/sys/class/dmi/id/sys_vendor", + "/sys/devices/virtual/dmi/id/bios_vendor", + } + + for _, path := range vendorPaths { + data, err := os.ReadFile(path) + if err != nil { + continue + } + vendor := strings.ToLower(strings.TrimSpace(string(data))) + + switch { + case strings.Contains(vendor, "amazon"): + return "aws" + case strings.Contains(vendor, "google"): + return "gcp" + case strings.Contains(vendor, "microsoft"): + return "azure" + } + } + + return "local" +} diff --git a/internal/executor/platform_test.go b/internal/executor/platform_test.go new file mode 100644 index 0000000..bf0bc68 --- /dev/null +++ b/internal/executor/platform_test.go @@ -0,0 +1,201 @@ +package executor + +import ( + "runtime" + "testing" + + "github.com/j3ssie/osmedeus/v5/internal/config" + "github.com/j3ssie/osmedeus/v5/internal/core" + "github.com/j3ssie/osmedeus/v5/internal/template" +) + +func TestDetectDocker(t *testing.T) { + result := DetectDocker() + // On a non-Docker environment, this should be false + t.Logf("DetectDocker() = %v", result) + // This is primarily a smoke test - we just verify it doesn't panic +} + +func TestDetectKubernetes(t *testing.T) { + result := DetectKubernetes() + // On a non-Kubernetes environment, this should be false + t.Logf("DetectKubernetes() = %v", result) + // This is primarily a smoke test - we just verify it doesn't panic +} + +func TestDetectCloudProvider(t *testing.T) { + result := DetectCloudProvider() + t.Logf("DetectCloudProvider() = %s", result) + // On a local machine, this should be "local" + // This is primarily a smoke test - we just verify it doesn't panic +} + +func TestPlatformVariables(t *testing.T) { + // Verify that runtime.GOOS and runtime.GOARCH return expected values + t.Logf("runtime.GOOS = %s", runtime.GOOS) + t.Logf("runtime.GOARCH = %s", runtime.GOARCH) + + // Verify they are not empty + if runtime.GOOS == "" { + t.Error("runtime.GOOS is empty") + } + if runtime.GOARCH == "" { + t.Error("runtime.GOARCH is empty") + } +} + +func TestPlatformVariablesInjection(t *testing.T) { + // Create a minimal config + cfg := &config.Config{ + BaseFolder: "/tmp", + BinariesPath: "/tmp/bin", + DataPath: "/tmp/data", + WorkspacesPath: "/tmp/workspaces", + } + + // Create execution context + execCtx := core.NewExecutionContext("test-workflow", core.KindModule, "test-run-uuid", "example.com") + + // Create executor and inject variables + e := NewExecutor() + params := map[string]string{ + "target": "example.com", + } + e.injectBuiltinVariables(cfg, params, execCtx) + + // Verify platform variables are set + tests := []struct { + name string + expected interface{} + }{ + {"PlatformOS", runtime.GOOS}, + {"PlatformArch", runtime.GOARCH}, + {"PlatformInDocker", DetectDocker()}, + {"PlatformInKubernetes", DetectKubernetes()}, + {"PlatformCloudProvider", DetectCloudProvider()}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, ok := execCtx.GetVariable(tt.name) + if !ok { + t.Errorf("variable %s not found in execution context", tt.name) + return + } + if val != tt.expected { + t.Errorf("variable %s = %v, expected %v", tt.name, val, tt.expected) + } + t.Logf("%s = %v", tt.name, val) + }) + } + + // Verify variables are in GetVariables() output + allVars := execCtx.GetVariables() + for _, tt := range tests { + if _, ok := allVars[tt.name]; !ok { + t.Errorf("variable %s not found in GetVariables() output", tt.name) + } + } +} + +func TestPlatformVariablesTemplateRendering(t *testing.T) { + // Create a minimal config + cfg := &config.Config{ + BaseFolder: "/tmp", + BinariesPath: "/tmp/bin", + DataPath: "/tmp/data", + WorkspacesPath: "/tmp/workspaces", + } + + // Create execution context + execCtx := core.NewExecutionContext("test-workflow", core.KindModule, "test-run-uuid", "example.com") + + // Create executor and inject variables + e := NewExecutor() + params := map[string]string{ + "target": "example.com", + } + e.injectBuiltinVariables(cfg, params, execCtx) + + // Create template engine and test rendering + engine := template.NewEngine() + vars := execCtx.GetVariables() + + // Debug: print all variables + t.Logf("Total variables: %d", len(vars)) + for k, v := range vars { + if k == "PlatformOS" || k == "PlatformArch" || k == "PlatformInDocker" || k == "PlatformInKubernetes" || k == "PlatformCloudProvider" { + t.Logf("Variable %s = %v (type: %T)", k, v, v) + } + } + + tests := []struct { + template string + expected string + }{ + {"echo {{PlatformOS}}", "echo " + runtime.GOOS}, + {"echo {{PlatformArch}}", "echo " + runtime.GOARCH}, + {"echo {{PlatformInDocker}}", "echo false"}, + {"echo {{PlatformInKubernetes}}", "echo false"}, + {"echo {{PlatformCloudProvider}}", "echo local"}, + } + + for _, tt := range tests { + t.Run(tt.template, func(t *testing.T) { + result, err := engine.Render(tt.template, vars) + if err != nil { + t.Errorf("error rendering template %q: %v", tt.template, err) + return + } + if result != tt.expected { + t.Errorf("render(%q) = %q, expected %q", tt.template, result, tt.expected) + } + t.Logf("render(%q) = %q", tt.template, result) + }) + } +} + +func TestPlatformVariablesWithStepDispatcher(t *testing.T) { + // Create a minimal config + cfg := &config.Config{ + BaseFolder: "/tmp", + BinariesPath: "/tmp/bin", + DataPath: "/tmp/data", + WorkspacesPath: "/tmp/workspaces", + } + + // Create execution context + execCtx := core.NewExecutionContext("test-workflow", core.KindModule, "test-run-uuid", "example.com") + + // Create executor and inject variables + e := NewExecutor() + params := map[string]string{ + "target": "example.com", + } + e.injectBuiltinVariables(cfg, params, execCtx) + + // Get the step dispatcher + dispatcher := NewStepDispatcher() + + // Create a test step + step := &core.Step{ + Name: "test-platform-step", + Type: core.StepTypeBash, + Command: "echo {{PlatformOS}} {{PlatformArch}}", + } + + // Get the template engine from dispatcher and render + vars := execCtx.GetVariables() + engine := dispatcher.GetTemplateEngine() + + result, err := engine.Render(step.Command, vars) + if err != nil { + t.Fatalf("Failed to render command: %v", err) + } + + expected := "echo " + runtime.GOOS + " " + runtime.GOARCH + if result != expected { + t.Errorf("Rendered command = %q, expected %q", result, expected) + } + t.Logf("Rendered command: %q", result) +} diff --git a/pkg/cli/function.go b/pkg/cli/function.go index 1e50046..82cba81 100644 --- a/pkg/cli/function.go +++ b/pkg/cli/function.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/signal" + "runtime" "strings" "sync" "syscall" @@ -13,6 +14,7 @@ import ( "github.com/j3ssie/osmedeus/v5/internal/config" "github.com/j3ssie/osmedeus/v5/internal/database" + "github.com/j3ssie/osmedeus/v5/internal/executor" "github.com/j3ssie/osmedeus/v5/internal/functions" "github.com/j3ssie/osmedeus/v5/internal/template" "github.com/j3ssie/osmedeus/v5/internal/terminal" @@ -290,6 +292,14 @@ func runBulkFunctionEval(printer *terminal.Printer, script string) error { func executeFunctionForTarget(printer *terminal.Printer, script, target string) error { // Build context with target and params ctx := make(map[string]interface{}) + + // Inject platform detection variables (always available, even without target) + ctx["PlatformOS"] = runtime.GOOS + ctx["PlatformArch"] = runtime.GOARCH + ctx["PlatformInDocker"] = executor.DetectDocker() + ctx["PlatformInKubernetes"] = executor.DetectKubernetes() + ctx["PlatformCloudProvider"] = executor.DetectCloudProvider() + if target != "" { ctx["target"] = target }