feat(ansible): expand extra-vars parsing

This commit is contained in:
Virgil 2026-04-02 13:21:01 +00:00
parent ff2a8e7731
commit bde3c18e19
2 changed files with 140 additions and 21 deletions

View file

@ -2,6 +2,7 @@ package anscmd
import (
"context"
"encoding/json"
"os"
"strconv"
"strings"
@ -11,6 +12,7 @@ import (
"dappco.re/go/core/ansible"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
"gopkg.in/yaml.v3"
)
// args extracts all positional arguments from Options.
@ -77,8 +79,8 @@ func verbosityLevel(opts core.Options, rawArgs []string) int {
}
// extraVars collects all repeated extra-vars values from Options.
func extraVars(opts core.Options) map[string]string {
vars := make(map[string]string)
func extraVars(opts core.Options) (map[string]any, error) {
vars := make(map[string]any)
for _, o := range opts.Items() {
if o.Key != "extra-vars" && o.Key != "e" {
@ -100,22 +102,83 @@ func extraVars(opts core.Options) map[string]string {
}
for _, value := range values {
for _, pair := range split(value, ",") {
parts := splitN(pair, "=", 2)
if len(parts) != 2 {
continue
}
key := trimSpace(parts[0])
if key == "" {
continue
}
vars[key] = parts[1]
parsed, err := parseExtraVarsValue(value)
if err != nil {
return nil, err
}
for key, parsedValue := range parsed {
vars[key] = parsedValue
}
}
}
return vars, nil
}
func parseExtraVarsValue(value string) (map[string]any, error) {
trimmed := trimSpace(value)
if trimmed == "" {
return nil, nil
}
if strings.HasPrefix(trimmed, "@") {
path := trimSpace(strings.TrimPrefix(trimmed, "@"))
if path == "" {
return nil, coreerr.E("parseExtraVarsValue", "extra vars file path required", nil)
}
data, err := coreio.Local.Read(path)
if err != nil {
return nil, coreerr.E("parseExtraVarsValue", "read extra vars file", err)
}
return parseExtraVarsValue(string(data))
}
if structured, ok := parseStructuredExtraVars(trimmed); ok {
return structured, nil
}
if strings.Contains(trimmed, "=") {
return parseKeyValueExtraVars(trimmed), nil
}
return nil, nil
}
func parseStructuredExtraVars(value string) (map[string]any, bool) {
var parsed map[string]any
if json.Valid([]byte(value)) {
if err := yaml.Unmarshal([]byte(value), &parsed); err == nil && len(parsed) > 0 {
return parsed, true
}
}
if err := yaml.Unmarshal([]byte(value), &parsed); err != nil {
return nil, false
}
if len(parsed) == 0 {
return nil, false
}
return parsed, true
}
func parseKeyValueExtraVars(value string) map[string]any {
vars := make(map[string]any)
for _, pair := range split(value, ",") {
parts := splitN(pair, "=", 2)
if len(parts) != 2 {
continue
}
key := trimSpace(parts[0])
if key == "" {
continue
}
vars[key] = parts[1]
}
return vars
}
@ -162,7 +225,11 @@ func runAnsible(opts core.Options) core.Result {
}
// Parse extra vars
for key, value := range extraVars(opts) {
vars, err := extraVars(opts)
if err != nil {
return core.Result{Value: coreerr.E("runAnsible", "parse extra vars", err)}
}
for key, value := range vars {
executor.SetVar(key, value)
}

View file

@ -1,6 +1,8 @@
package anscmd
import (
"os"
"path/filepath"
"testing"
"dappco.re/go/core"
@ -15,9 +17,10 @@ func TestExtraVars_Good_RepeatableAndCommaSeparated(t *testing.T) {
core.Option{Key: "extra-vars", Value: []string{"build=42"}},
)
vars := extraVars(opts)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]string{
assert.Equal(t, map[string]any{
"version": "1.2.3",
"env": "prod",
"region": "us-east-1",
@ -30,9 +33,10 @@ func TestExtraVars_Good_UsesShortAlias(t *testing.T) {
core.Option{Key: "e", Value: "version=1.2.3,env=prod"},
)
vars := extraVars(opts)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]string{
assert.Equal(t, map[string]any{
"version": "1.2.3",
"env": "prod",
}, vars)
@ -44,14 +48,62 @@ func TestExtraVars_Good_IgnoresMalformedPairs(t *testing.T) {
core.Option{Key: "extra-vars", Value: "also_bad="},
)
vars := extraVars(opts)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]string{
assert.Equal(t, map[string]any{
"keep": "this",
"also_bad": "",
}, vars)
}
func TestExtraVars_Good_SupportsStructuredYAMLAndJSON(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "app:\n port: 8080\n debug: true"},
core.Option{Key: "extra-vars", Value: `{"image":"nginx:latest","replicas":3}`},
)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"app": map[string]any{
"port": int(8080),
"debug": true,
},
"image": "nginx:latest",
"replicas": int(3),
}, vars)
}
func TestExtraVars_Good_LoadsFileReferences(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "vars.yml")
require.NoError(t, os.WriteFile(path, []byte("deploy_env: prod\nrelease: 42\n"), 0644))
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "@" + path},
)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"deploy_env": "prod",
"release": int(42),
}, vars)
}
func TestExtraVars_Bad_MissingFile(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "@/definitely/missing/vars.yml"},
)
_, err := extraVars(opts)
require.Error(t, err)
assert.Contains(t, err.Error(), "read extra vars file")
}
func TestFirstString_Good_PrefersFirstNonEmptyKey(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "inventory", Value: ""},