From 34229558fbca6ef5d7e57b28d5e59e0e6a799ac9 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 19:05:24 +0000 Subject: [PATCH] fix(ansible): implement include_vars loading --- executor_extra_test.go | 20 +++++++-- modules.go | 99 +++++++++++++++++++++++++++++++++++++++--- modules_adv_test.go | 51 ++++++++++++++++++++++ 3 files changed, 161 insertions(+), 9 deletions(-) diff --git a/executor_extra_test.go b/executor_extra_test.go index d66e725..d9e843f 100644 --- a/executor_extra_test.go +++ b/executor_extra_test.go @@ -150,19 +150,31 @@ func TestExecutorExtra_ModuleSetFact_Good_SkipsCacheable(t *testing.T) { // --- moduleIncludeVars --- func TestExecutorExtra_ModuleIncludeVars_Good_WithFile(t *testing.T) { + dir := t.TempDir() + path := joinPath(dir, "main.yml") + require.NoError(t, writeTestFile(path, []byte("app_name: demo\n"), 0644)) + e := NewExecutor("/tmp") - result, err := e.moduleIncludeVars(map[string]any{"file": "vars/main.yml"}) + result, err := e.moduleIncludeVars(map[string]any{"file": path}) require.NoError(t, err) - assert.Contains(t, result.Msg, "vars/main.yml") + assert.True(t, result.Changed) + assert.Contains(t, result.Msg, path) + assert.Equal(t, "demo", e.vars["app_name"]) } func TestExecutorExtra_ModuleIncludeVars_Good_WithRawParams(t *testing.T) { + dir := t.TempDir() + path := joinPath(dir, "defaults.yml") + require.NoError(t, writeTestFile(path, []byte("app_port: 8080\n"), 0644)) + e := NewExecutor("/tmp") - result, err := e.moduleIncludeVars(map[string]any{"_raw_params": "defaults.yml"}) + result, err := e.moduleIncludeVars(map[string]any{"_raw_params": path}) require.NoError(t, err) - assert.Contains(t, result.Msg, "defaults.yml") + assert.True(t, result.Changed) + assert.Contains(t, result.Msg, path) + assert.Equal(t, 8080, e.vars["app_port"]) } func TestExecutorExtra_ModuleIncludeVars_Good_Empty(t *testing.T) { diff --git a/modules.go b/modules.go index 0c4e189..9a4d0b0 100644 --- a/modules.go +++ b/modules.go @@ -4,10 +4,14 @@ import ( "context" "encoding/base64" "io/fs" + "os" + "path/filepath" + "sort" "strconv" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + "gopkg.in/yaml.v3" ) // executeModule dispatches to the appropriate module handler. @@ -1261,14 +1265,99 @@ func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) { if file == "" { file = getStringArg(args, "_raw_params", "") } + dir := getStringArg(args, "dir", "") + name := getStringArg(args, "name", "") + hashBehaviour := lower(getStringArg(args, "hash_behaviour", "replace")) - if file != "" { - // Would need to read and parse the vars file - // For now, just acknowledge - return &TaskResult{Changed: false, Msg: "include_vars: " + file}, nil + if file == "" && dir == "" { + return &TaskResult{Changed: false}, nil } - return &TaskResult{Changed: false}, nil + loaded := make(map[string]any) + var sources []string + loadFile := func(path string) error { + data, err := coreio.Local.Read(path) + if err != nil { + return coreerr.E("Executor.moduleIncludeVars", "read vars file", err) + } + + var vars map[string]any + if err := yaml.Unmarshal([]byte(data), &vars); err != nil { + return coreerr.E("Executor.moduleIncludeVars", "parse vars file", err) + } + + mergeVars(loaded, vars, hashBehaviour == "merge") + return nil + } + + if file != "" { + sources = append(sources, file) + if err := loadFile(file); err != nil { + return nil, err + } + } + + if dir != "" { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, coreerr.E("Executor.moduleIncludeVars", "read vars dir", err) + } + + var files []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + + ext := lower(filepath.Ext(entry.Name())) + if ext == ".yml" || ext == ".yaml" { + files = append(files, joinPath(dir, entry.Name())) + } + } + sort.Strings(files) + + for _, path := range files { + sources = append(sources, path) + if err := loadFile(path); err != nil { + return nil, err + } + } + } + + if name != "" { + e.vars[name] = loaded + } else { + mergeVars(e.vars, loaded, hashBehaviour == "merge") + } + + msg := "include_vars" + if len(sources) > 0 { + msg += ": " + join(", ", sources) + } + + return &TaskResult{Changed: true, Msg: msg}, nil +} + +func mergeVars(dst, src map[string]any, mergeMaps bool) { + if dst == nil || src == nil { + return + } + + for key, val := range src { + if !mergeMaps { + dst[key] = val + continue + } + + if existing, ok := dst[key].(map[string]any); ok { + if next, ok := val.(map[string]any); ok { + mergeVars(existing, next, true) + continue + } + } + + dst[key] = val + } } func (e *Executor) moduleMeta(args map[string]any) (*TaskResult, error) { diff --git a/modules_adv_test.go b/modules_adv_test.go index e336c8e..0d3bab5 100644 --- a/modules_adv_test.go +++ b/modules_adv_test.go @@ -702,6 +702,57 @@ func TestModulesAdv_ModuleUnarchive_Bad_LocalFileNotFound(t *testing.T) { assert.Contains(t, err.Error(), "read src") } +// --- include_vars module --- + +func TestModulesAdv_ModuleIncludeVars_Good_LoadSingleFile(t *testing.T) { + dir := t.TempDir() + varsPath := joinPath(dir, "vars.yml") + require.NoError(t, writeTestFile(varsPath, []byte("app_name: demo\napp_port: 8080\nnested:\n enabled: true\n"), 0644)) + + e := NewExecutor("/tmp") + + result, err := e.moduleIncludeVars(map[string]any{ + "file": varsPath, + }) + + require.NoError(t, err) + assert.True(t, result.Changed) + assert.False(t, result.Failed) + assert.Contains(t, result.Msg, varsPath) + assert.Equal(t, "demo", e.vars["app_name"]) + assert.Equal(t, 8080, e.vars["app_port"]) + + nested, ok := e.vars["nested"].(map[string]any) + require.True(t, ok) + assert.Equal(t, true, nested["enabled"]) +} + +func TestModulesAdv_ModuleIncludeVars_Good_LoadDirectoryWithMerge(t *testing.T) { + dir := t.TempDir() + require.NoError(t, writeTestFile(joinPath(dir, "01-base.yml"), []byte("app_name: demo\nnested:\n a: 1\n"), 0644)) + require.NoError(t, writeTestFile(joinPath(dir, "02-override.yaml"), []byte("app_port: 8080\nnested:\n b: 2\n"), 0644)) + + e := NewExecutor("/tmp") + + result, err := e.moduleIncludeVars(map[string]any{ + "dir": dir, + "hash_behaviour": "merge", + }) + + require.NoError(t, err) + assert.True(t, result.Changed) + assert.False(t, result.Failed) + assert.Contains(t, result.Msg, joinPath(dir, "01-base.yml")) + assert.Contains(t, result.Msg, joinPath(dir, "02-override.yaml")) + assert.Equal(t, "demo", e.vars["app_name"]) + assert.Equal(t, 8080, e.vars["app_port"]) + + nested, ok := e.vars["nested"].(map[string]any) + require.True(t, ok) + assert.Equal(t, 1, nested["a"]) + assert.Equal(t, 2, nested["b"]) +} + // --- uri module --- func TestModulesAdv_ModuleURI_Good_GetRequestDefault(t *testing.T) {