diff --git a/README.md b/README.md index 9c493fbf..42af1533 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,61 @@ Save the YAML content to `x.vals.yaml` and running `vals eval -f x.vals.yaml` do foo: myvalue ``` +### Raw Text Mode + +By default, `vals eval` expects valid YAML or JSON input. However, you can use the `--raw` flag to treat input as plain text, similar to `envsubst` but with the `ref+` syntax. This enables support for: + +- Plain text files +- JSONC (JSON with comments) +- JSON5 +- Configuration files +- Any text format where you want to replace `ref+` expressions + +**Example with plain text:** + +```console +$ echo 'DATABASE_URL=ref+echo://postgres://localhost:5432/mydb' | vals eval -f - --raw +DATABASE_URL=postgres://localhost:5432/mydb +``` + +**Example with JSONC (JSON with comments):** + +```console +$ cat config.jsonc +{ + // Database configuration + "database": "ref+vault://secret/data/db#/url", + /* API Keys */ + "api_key": "ref+awssecrets://prod/api#/key" +} + +$ vals eval -f config.jsonc --raw +{ + // Database configuration + "database": "postgres://prod-db:5432/myapp", + /* API Keys */ + "api_key": "sk-prod-1234567890" +} +``` + +**Example with configuration file:** + +```console +$ cat app.conf +# Application Configuration +DATABASE_URL=ref+awsssm://prod/db/url +API_KEY=ref+vault://secret/data/api#/key +REDIS_URL=ref+awsssm://prod/redis/url + +$ vals eval -f app.conf --raw +# Application Configuration +DATABASE_URL=postgres://prod-db:5432/myapp +API_KEY=my-secret-api-key +REDIS_URL=redis://prod-redis:6379 +``` + +The `--raw` flag preserves the original file format and structure while replacing all `ref+` expressions with their actual values. + ### Helm Use value references as Helm Chart values, so that you can feed the `helm template` output to `vals -f -` for transforming the refs to secrets. diff --git a/cmd/vals/main.go b/cmd/vals/main.go index b3b46fd4..250cfb98 100644 --- a/cmd/vals/main.go +++ b/cmd/vals/main.go @@ -94,6 +94,7 @@ func main() { e := evalCmd.Bool("exclude-secret", false, "Leave secretref+ as-is and only replace ref+") k := evalCmd.Bool("decode-kubernetes-secrets", false, "Decode Kubernetes secrets before evaluate them, then encode it again.") failOnMissingKeyInMap := evalCmd.Bool("fail-on-missing-key-in-map", true, "When set to false, the vals-eval command exits with code 0 even when the key denoted by the #/key/for/value/in/the/json/or/yaml does not exist in the decoded map") + raw := evalCmd.Bool("raw", false, "Treat input as raw text (like envsubst) instead of YAML/JSON. Enables support for plain text, JSONC, and JSON5 files. Note: -o flag is ignored in raw mode") err := evalCmd.Parse(os.Args[2:]) if err != nil { fatal("%v", err) @@ -104,6 +105,29 @@ func main() { logOut = io.Discard } + // Handle raw text mode + if *raw { + content, err := vals.RawInput(*f) + if err != nil { + fatal("%v", err) + } + + result, err := vals.Get(content, vals.Options{ + ExcludeSecret: *e, + LogOutput: logOut, + FailOnMissingKeyInMap: *failOnMissingKeyInMap, + }) + if err != nil { + fatal("%v", err) + } + + _, err = os.Stdout.WriteString(result) + if err != nil { + fatal("%v", err) + } + return + } + nodes := readNodesOrFail(f) if *k { diff --git a/io.go b/io.go index a332e43e..227631a3 100644 --- a/io.go +++ b/io.go @@ -97,6 +97,31 @@ func replaceTimestamp(n *yaml.Node) { } } +// RawInput reads the raw text content from a file or stdin +func RawInput(f string) (string, error) { + var reader io.Reader + if f == "-" { + reader = os.Stdin + } else if f != "" { + fp, err := os.Open(f) + if err != nil { + return "", err + } + defer func() { + _ = fp.Close() + }() + reader = fp + } else { + return "", fmt.Errorf("Nothing to eval: No file specified") + } + + content, err := io.ReadAll(reader) + if err != nil { + return "", err + } + return string(content), nil +} + func Output(output io.Writer, format string, nodes []yaml.Node) error { for i, node := range nodes { var v interface{} diff --git a/io_test.go b/io_test.go index 9f9e1e01..c3a74cfd 100644 --- a/io_test.go +++ b/io_test.go @@ -2,6 +2,7 @@ package vals import ( "bytes" + "os" "strings" "testing" ) @@ -127,3 +128,51 @@ func Test_NodesFromReader(t *testing.T) { }) } } + +func Test_RawInput(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "plain text", + input: "plain text content", + expected: "plain text content", + }, + { + name: "text with newlines", + input: "line1\nline2\nline3", + expected: "line1\nline2\nline3", + }, + { + name: "JSONC with comments", + input: "{\n // comment\n \"key\": \"value\"\n}", + expected: "{\n // comment\n \"key\": \"value\"\n}", + }, + { + name: "JSON5 with trailing comma", + input: "{\n key: 'value',\n}", + expected: "{\n key: 'value',\n}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temp file with the input content + tmpFile := t.TempDir() + "/test.txt" + if err := os.WriteFile(tmpFile, []byte(tt.input), 0644); err != nil { + t.Fatal(err) + } + + result, err := RawInput(tmpFile) + if err != nil { + t.Fatal(err) + } + + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} diff --git a/vals_test.go b/vals_test.go index 9fdfe9de..82f885ec 100644 --- a/vals_test.go +++ b/vals_test.go @@ -187,3 +187,35 @@ datetime_offset: "2025-01-01T12:34:56+01:00" require.Equal(t, expected, buf.String()) } + +func TestGetRawText(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "plain text with ref", + input: "plain text with ref+echo://hello-world embedded", + expected: "plain text with hello-world embedded", + }, + { + name: "JSONC with refs", + input: "{\n // comment\n \"key\": \"ref+echo://value\"\n}", + expected: "{\n // comment\n \"key\": \"value\"\n}", + }, + { + name: "config file", + input: "DATABASE_URL=ref+echo://postgres://localhost/db\nAPI_KEY=ref+echo://secret-123", + expected: "DATABASE_URL=postgres://localhost/db\nAPI_KEY=secret-123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Get(tt.input, Options{}) + require.NoError(t, err) + require.Equal(t, tt.expected, result) + }) + } +}