From 9e7219782af45c643b59bf4fd675c0596aa09991 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 23:05:24 +0000 Subject: [PATCH] feat(ansible): support play vars_files --- executor.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ executor_test.go | 37 +++++++++++++++++++++++++++++++++++++ types.go | 1 + 3 files changed, 84 insertions(+) diff --git a/executor.go b/executor.go index 34227d0..43367f4 100644 --- a/executor.go +++ b/executor.go @@ -18,6 +18,7 @@ import ( coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + "gopkg.in/yaml.v3" ) var errEndPlay = errors.New("end play") @@ -183,6 +184,9 @@ func (e *Executor) runPlay(ctx context.Context, play *Play) error { e.endedHosts = make(map[string]bool) // Merge play vars + if err := e.loadPlayVarsFiles(play); err != nil { + return err + } for k, v := range play.Vars { e.vars[k] = v } @@ -279,6 +283,48 @@ func (e *Executor) runPlay(ctx context.Context, play *Play) error { return nil } +// loadPlayVarsFiles loads any play-level vars_files entries and merges them +// into the play's Vars map before execution begins. +func (e *Executor) loadPlayVarsFiles(play *Play) error { + if play == nil { + return nil + } + + files := normalizeStringList(play.VarsFiles) + if len(files) == 0 { + return nil + } + + merged := make(map[string]any) + for _, file := range files { + resolved := e.resolveLocalPath(file) + data, err := coreio.Local.Read(resolved) + if err != nil { + return coreerr.E("Executor.loadPlayVarsFiles", "read vars file", err) + } + + var vars map[string]any + if err := yaml.Unmarshal([]byte(data), &vars); err != nil { + return coreerr.E("Executor.loadPlayVarsFiles", "parse vars file", err) + } + + mergeVars(merged, vars, false) + } + + if len(merged) == 0 { + return nil + } + + if play.Vars == nil { + play.Vars = make(map[string]any) + } + + mergeVars(merged, play.Vars, false) + play.Vars = merged + + return nil +} + // splitSerialHosts splits a host list into serial batches. func splitSerialHosts(hosts []string, serial any) [][]string { batchSize := resolveSerialBatchSize(serial, len(hosts)) diff --git a/executor_test.go b/executor_test.go index c3a4faa..3257387 100644 --- a/executor_test.go +++ b/executor_test.go @@ -249,6 +249,43 @@ func TestExecutor_RunTaskOnHost_Good_DelegateToUsesDelegatedClient(t *testing.T) assert.Equal(t, 1, mock.commandCount()) } +func TestExecutor_Run_Good_VarsFilesMergeIntoPlayVars(t *testing.T) { + dir := t.TempDir() + + require.NoError(t, writeTestFile(joinPath(dir, "vars", "common.yml"), []byte(`--- +http_port: 8080 +app_name: base +environment: staging +`), 0644)) + require.NoError(t, writeTestFile(joinPath(dir, "vars", "override.yml"), []byte(`--- +app_name: demo +`), 0644)) + + playbookPath := joinPath(dir, "playbook.yml") + require.NoError(t, writeTestFile(playbookPath, []byte(`--- +- name: Vars files + hosts: localhost + gather_facts: false + vars: + environment: prod + vars_files: + - vars/common.yml + - vars/override.yml + tasks: + - name: Show merged vars + debug: + msg: "{{ http_port }} {{ app_name }} {{ environment }}" + register: vars_result +`), 0644)) + + e := NewExecutor(dir) + require.NoError(t, e.Run(context.Background(), playbookPath)) + + require.NotNil(t, e.results["localhost"]) + require.NotNil(t, e.results["localhost"]["vars_result"]) + assert.Equal(t, "8080 demo prod", e.results["localhost"]["vars_result"].Msg) +} + func TestExecutor_RunTaskOnHosts_Good_WithFileUsesFileContents(t *testing.T) { dir := t.TempDir() require.NoError(t, writeTestFile(joinPath(dir, "fragments", "hello.txt"), []byte("hello from file"), 0644)) diff --git a/types.go b/types.go index ecd613f..77f1a77 100644 --- a/types.go +++ b/types.go @@ -27,6 +27,7 @@ type Play struct { BecomeUser string `yaml:"become_user,omitempty"` GatherFacts *bool `yaml:"gather_facts,omitempty"` Vars map[string]any `yaml:"vars,omitempty"` + VarsFiles any `yaml:"vars_files,omitempty"` // string or []string PreTasks []Task `yaml:"pre_tasks,omitempty"` Tasks []Task `yaml:"tasks,omitempty"` PostTasks []Task `yaml:"post_tasks,omitempty"`