From bfa9a8d0ba4058ba02e83c9181c678dd062d241c Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 23:27:19 +0000 Subject: [PATCH] feat(ansible): recurse include_vars directories --- modules.go | 65 ++++++++++++++++++++++++++++++++++----------- modules_adv_test.go | 44 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/modules.go b/modules.go index 7a4c1ab..26c7407 100644 --- a/modules.go +++ b/modules.go @@ -2115,6 +2115,7 @@ func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) { dir := getStringArg(args, "dir", "") name := getStringArg(args, "name", "") hashBehaviour := lower(getStringArg(args, "hash_behaviour", "replace")) + depth := getIntArg(args, "depth", 0) if file == "" && dir == "" { return &TaskResult{Changed: false}, nil @@ -2147,24 +2148,11 @@ func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) { if dir != "" { dir = e.resolveLocalPath(dir) - entries, err := os.ReadDir(dir) + files, err := collectIncludeVarsFiles(dir, depth) if err != nil { - return nil, coreerr.E("Executor.moduleIncludeVars", "read vars dir", err) + return nil, 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 { @@ -2187,6 +2175,53 @@ func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) { return &TaskResult{Changed: true, Msg: msg}, nil } +func collectIncludeVarsFiles(dir string, depth int) ([]string, error) { + info, err := os.Stat(dir) + if err != nil { + return nil, coreerr.E("Executor.moduleIncludeVars", "read vars dir", err) + } + if !info.IsDir() { + return nil, coreerr.E("Executor.moduleIncludeVars", "read vars dir: not a directory", nil) + } + + type dirEntry struct { + path string + depth int + } + + var files []string + stack := []dirEntry{{path: dir, depth: 0}} + for len(stack) > 0 { + current := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + entries, err := os.ReadDir(current.path) + if err != nil { + return nil, coreerr.E("Executor.moduleIncludeVars", "read vars dir", err) + } + + for i := len(entries) - 1; i >= 0; i-- { + entry := entries[i] + fullPath := joinPath(current.path, entry.Name()) + + if entry.IsDir() { + if depth == 0 || current.depth < depth { + stack = append(stack, dirEntry{path: fullPath, depth: current.depth + 1}) + } + continue + } + + ext := lower(filepath.Ext(entry.Name())) + if ext == ".yml" || ext == ".yaml" { + files = append(files, fullPath) + } + } + } + + sort.Strings(files) + return files, nil +} + func mergeVars(dst, src map[string]any, mergeMaps bool) { if dst == nil || src == nil { return diff --git a/modules_adv_test.go b/modules_adv_test.go index ac68d0b..2f2d5ac 100644 --- a/modules_adv_test.go +++ b/modules_adv_test.go @@ -954,6 +954,50 @@ func TestModulesAdv_ModuleIncludeVars_Good_ResolvesRelativePathsAgainstBasePath( assert.Equal(t, 8080, e.vars["app_port"]) } +func TestModulesAdv_ModuleIncludeVars_Good_RecursesIntoNestedDirectories(t *testing.T) { + dir := t.TempDir() + require.NoError(t, writeTestFile(joinPath(dir, "01-root.yml"), []byte("root_value: root\n"), 0644)) + require.NoError(t, writeTestFile(joinPath(dir, "nested", "02-child.yaml"), []byte("child_value: child\n"), 0644)) + require.NoError(t, writeTestFile(joinPath(dir, "nested", "deep", "03-grandchild.yml"), []byte("grandchild_value: grandchild\n"), 0644)) + + e := NewExecutor("/tmp") + + result, err := e.moduleIncludeVars(map[string]any{ + "dir": dir, + }) + + require.NoError(t, err) + assert.True(t, result.Changed) + assert.Equal(t, "root", e.vars["root_value"]) + assert.Equal(t, "child", e.vars["child_value"]) + assert.Equal(t, "grandchild", e.vars["grandchild_value"]) + assert.Contains(t, result.Msg, joinPath(dir, "01-root.yml")) + assert.Contains(t, result.Msg, joinPath(dir, "nested", "02-child.yaml")) + assert.Contains(t, result.Msg, joinPath(dir, "nested", "deep", "03-grandchild.yml")) +} + +func TestModulesAdv_ModuleIncludeVars_Good_RespectsDepthLimit(t *testing.T) { + dir := t.TempDir() + require.NoError(t, writeTestFile(joinPath(dir, "01-root.yml"), []byte("root_value: root\n"), 0644)) + require.NoError(t, writeTestFile(joinPath(dir, "nested", "02-child.yaml"), []byte("child_value: child\n"), 0644)) + require.NoError(t, writeTestFile(joinPath(dir, "nested", "deep", "03-grandchild.yml"), []byte("grandchild_value: grandchild\n"), 0644)) + + e := NewExecutor("/tmp") + + result, err := e.moduleIncludeVars(map[string]any{ + "dir": dir, + "depth": 1, + }) + + require.NoError(t, err) + assert.True(t, result.Changed) + assert.Equal(t, "root", e.vars["root_value"]) + assert.Equal(t, "child", e.vars["child_value"]) + _, hasGrandchild := e.vars["grandchild_value"] + assert.False(t, hasGrandchild) + assert.NotContains(t, result.Msg, joinPath(dir, "nested", "deep", "03-grandchild.yml")) +} + // --- sysctl module --- func TestModulesAdv_ModuleSysctl_Good_ReloadsAfterPersisting(t *testing.T) {