feat(ansible): expand extra-vars parsing
This commit is contained in:
parent
ff2a8e7731
commit
bde3c18e19
2 changed files with 140 additions and 21 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: ""},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue