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
53 changes: 28 additions & 25 deletions engine/pkg/config/envvar/envvar.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Package envvar expands "${VAR}" and "$VAR" placeholders in selected config
// fields using the process environment.
// Package envvar expands a config field whose value is solely a "${VAR}" or
// "$VAR" placeholder using the process environment. A value that merely
// contains a "$" (a password, a regex backreference, an unmatched brace) is a
// literal and is left unchanged.
package envvar

import (
"os"
"regexp"

"github.com/pkg/errors"
)
Expand All @@ -14,35 +17,35 @@ type Field struct {
Ptr *string
}

// ExpandStrict expands "${VAR}" and "$VAR" references in s. It returns an
// error if any referenced variable is unset, so misconfigured tokens fail
// loudly at startup instead of silently resolving to an empty string.
// placeholderRE matches a value that consists solely of a single "${VAR}" or
// "$VAR" reference, capturing the variable name. Anything else (including a
// value that merely embeds a "$") is treated as a literal so secrets that
// legitimately contain a "$" survive load without corruption.
var placeholderRE = regexp.MustCompile(`^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$|^\$([A-Za-z_][A-Za-z0-9_]*)$`)

// ExpandStrict resolves s when it is exactly a single "${VAR}" or "$VAR"
// placeholder, returning the referenced environment variable's value. It
// returns an error if the referenced variable is unset, so a misconfigured
// placeholder fails loudly at startup instead of silently resolving to an
// empty string. Any other value, including one that merely contains a "$", is
// returned unchanged.
func ExpandStrict(s string) (string, error) {
if s == "" {
return "", nil
m := placeholderRE.FindStringSubmatch(s)
if m == nil {
return s, nil
}

var missing string

resolved := os.Expand(s, func(name string) string {
if missing != "" {
return ""
}

v, ok := os.LookupEnv(name)
if !ok {
missing = name
return ""
}

return v
})
name := m[1]
if name == "" {
name = m[2]
}

if missing != "" {
return "", errors.Errorf("environment variable %q is not set", missing)
v, ok := os.LookupEnv(name)
if !ok {
return "", errors.Errorf("environment variable %q is not set", name)
}

return resolved, nil
return v, nil
}

// ExpandFields applies ExpandStrict to each field and writes the resolved
Expand Down
9 changes: 8 additions & 1 deletion engine/pkg/config/envvar/envvar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func TestExpandStrict(t *testing.T) {
t.Setenv("DBLAB_TOKEN", "secret-from-env")
t.Setenv("EMPTY_VAR", "")
t.Setenv("en", "injected-value")

tests := []struct {
name string
Expand All @@ -23,7 +24,13 @@ func TestExpandStrict(t *testing.T) {
{name: "unbraced placeholder", input: "$DBLAB_TOKEN", want: "secret-from-env"},
{name: "explicitly empty env var", input: "${EMPTY_VAR}", want: ""},
{name: "unset variable", input: "${DBLAB_MISSING}", wantErr: `environment variable "DBLAB_MISSING" is not set`},
{name: "regex backreference looks like unset var", input: "***$1", wantErr: `environment variable "1" is not set`},
{name: "regex backreference preserved", input: "***$1", want: "***$1"},
{name: "password with dollar preserved", input: "pa$$w0rd", want: "pa$$w0rd"},
{name: "embedded reference not expanded", input: "tok$en", want: "tok$en"},
{name: "leading dollar embedded preserved", input: "$DBLAB_TOKEN-suffix", want: "$DBLAB_TOKEN-suffix"},
{name: "unclosed brace preserved", input: "${UNCLOSED", want: "${UNCLOSED"},
{name: "empty braces preserved", input: "${}", want: "${}"},
{name: "bare dollar preserved", input: "$", want: "$"},
}

for _, tc := range tests {
Expand Down
Loading