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.
This commit is contained in:
j3ssie
2026-01-24 16:22:44 +08:00
parent 9494f74942
commit e7eea69a40
4 changed files with 315 additions and 11 deletions
+25 -11
View File
@@ -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(),
+79
View File
@@ -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"
}
+201
View File
@@ -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)
}
+10
View File
@@ -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
}