Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions cmd/vals/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func main() {
e := evalCmd.Bool("exclude-secret", false, "Leave secretref+<uri> as-is and only replace ref+<uri>")
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)
Expand All @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions io.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
49 changes: 49 additions & 0 deletions io_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vals

import (
"bytes"
"os"
"strings"
"testing"
)
Expand Down Expand Up @@ -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)
}
})
}
}
32 changes: 32 additions & 0 deletions vals_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}