Skip to content

Commit aadaccb

Browse files
committed
feat(valkey-store): add username_env/password_env credential indirection Add support for resolving Valkey credentials from environment variables named in the model config, mirroring cloud-proxy's api_key_env pattern. This keeps secrets out of model YAML files and lets distinct store configs each reference their own credentials. Options: username_env / password_env name the env var holding the value. The direct username / password options still work and take precedence when both are set (backward compatible). Includes 5 unit tests and updated stores.md documentation.
Signed-off-by: Daria Korenieva <daric2612@gmail.com>
1 parent b7d7287 commit aadaccb

3 files changed

Lines changed: 71 additions & 4 deletions

File tree

backend/go/valkey-store/config.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package main
2323

2424
import (
2525
"fmt"
26+
"os"
2627
"strconv"
2728
"strings"
2829
"time"
@@ -76,6 +77,8 @@ const (
7677
optAddr = "addr"
7778
optUsername = "username"
7879
optPassword = "password"
80+
optUsernameEnv = "username_env"
81+
optPasswordEnv = "password_env"
7982
optTLS = "tls"
8083
optTLSSkipVerify = "tls_skip_verify"
8184
optTLSCACert = "tls_ca_cert"
@@ -164,8 +167,8 @@ func loadConfig(opts *pb.ModelOptions) (Config, error) {
164167

165168
cfg := Config{
166169
Addr: strOr(o, optAddr, _defaultAddr),
167-
Username: o[optUsername],
168-
Password: o[optPassword],
170+
Username: resolveCredential(o, optUsername, optUsernameEnv),
171+
Password: resolveCredential(o, optPassword, optPasswordEnv),
169172
UseTLS: boolOr(o, optTLS, false),
170173
TLSSkipVerify: boolOr(o, optTLSSkipVerify, false),
171174
TLSCACert: o[optTLSCACert],
@@ -236,3 +239,23 @@ func boolOr(o map[string]string, key string, fallback bool) bool {
236239
}
237240
return b
238241
}
242+
243+
// resolveCredential resolves a credential value with the following priority:
244+
// 1. Direct value from the model config option (e.g. `username:admin`)
245+
// 2. Env-indirection: if `username_env` names an env var, read the credential
246+
// from that variable (e.g. `username_env:MY_VALKEY_USER` → os.Getenv("MY_VALKEY_USER"))
247+
//
248+
// The env-indirection pattern (same as cloud-proxy's api_key_env) avoids putting
249+
// secrets directly in model YAML: distinct store configs can each reference a
250+
// different credential env var without any plaintext passwords in the config.
251+
func resolveCredential(o map[string]string, directKey, envKey string) string {
252+
// Direct value takes precedence (backward compatible).
253+
if v := o[directKey]; v != "" {
254+
return v
255+
}
256+
// Env indirection: the option names an env var that holds the credential.
257+
if envVar := o[envKey]; envVar != "" {
258+
return os.Getenv(envVar)
259+
}
260+
return ""
261+
}

backend/go/valkey-store/store_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,40 @@ var _ = Describe("loadConfig", func() {
111111
_, err := loadConfig(opts("db:-1"))
112112
Expect(err).To(HaveOccurred())
113113
})
114+
115+
It("resolves username from direct option", func() {
116+
cfg, err := loadConfig(opts("username:admin"))
117+
Expect(err).NotTo(HaveOccurred())
118+
Expect(cfg.Username).To(Equal("admin"))
119+
})
120+
121+
It("resolves password from env indirection via password_env", func() {
122+
GinkgoT().Setenv("TEST_VALKEY_PW_INDIRECT", "s3cret")
123+
cfg, err := loadConfig(opts("password_env:TEST_VALKEY_PW_INDIRECT"))
124+
Expect(err).NotTo(HaveOccurred())
125+
Expect(cfg.Password).To(Equal("s3cret"))
126+
})
127+
128+
It("resolves username from env indirection via username_env", func() {
129+
GinkgoT().Setenv("TEST_VALKEY_USER_INDIRECT", "myuser")
130+
cfg, err := loadConfig(opts("username_env:TEST_VALKEY_USER_INDIRECT"))
131+
Expect(err).NotTo(HaveOccurred())
132+
Expect(cfg.Username).To(Equal("myuser"))
133+
})
134+
135+
It("prefers the direct option over env indirection", func() {
136+
GinkgoT().Setenv("TEST_VALKEY_PW_CLASH", "from-env")
137+
cfg, err := loadConfig(opts("password:direct-value", "password_env:TEST_VALKEY_PW_CLASH"))
138+
Expect(err).NotTo(HaveOccurred())
139+
Expect(cfg.Password).To(Equal("direct-value"))
140+
})
141+
142+
It("returns empty when neither direct nor env indirection is set", func() {
143+
cfg, err := loadConfig(opts("addr:localhost:6379"))
144+
Expect(err).NotTo(HaveOccurred())
145+
Expect(cfg.Username).To(BeEmpty())
146+
Expect(cfg.Password).To(BeEmpty())
147+
})
114148
})
115149

116150
var _ = Describe("Load namespace gate", func() {

docs/content/features/stores.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,28 @@ name: my-vectors
8484
backend: valkey-store
8585
options:
8686
- addr:valkey.internal:6379
87+
- username_env:MY_VALKEY_USER
88+
- password_env:MY_VALKEY_PASSWORD
8789
- index_algo:HNSW
8890
- distance_metric:COSINE
8991
```
9092
93+
The `username_env` / `password_env` options name an environment variable that holds the actual
94+
credential rather than putting the secret directly in the YAML. This mirrors `cloud-proxy`'s
95+
`api_key_env` pattern and lets distinct store configs each reference their own credentials.
96+
The direct `username` / `password` options still work for backward compatibility, and take
97+
precedence when both are set.
98+
9199
When no config exists for a store, the backend connects to `localhost:6379` with the defaults
92100
below (so the zero-config experience still works).
93101

94102
| Option | Default | Description |
95103
|--------|---------|-------------|
96104
| `addr` | `localhost:6379` | Valkey server address (`host:port`). |
97-
| `username` | *(empty)* | Optional ACL username. |
98-
| `password` | *(empty)* | Optional password / ACL secret. |
105+
| `username` | *(empty)* | Optional ACL username (plaintext in config). |
106+
| `password` | *(empty)* | Optional password / ACL secret (plaintext in config). |
107+
| `username_env` | *(empty)* | Name of an env var that holds the username. Preferred over `username` for secrets — keeps credentials out of the model YAML. |
108+
| `password_env` | *(empty)* | Name of an env var that holds the password. Preferred over `password` for secrets. |
99109
| `tls` | `false` | Enable TLS (required by many managed deployments). |
100110
| `tls_ca_cert` | *(empty)* | Path to a PEM CA bundle used to verify the server certificate (self-signed / private CA). |
101111
| `tls_skip_verify` | `false` | Skip TLS certificate verification. Insecure — for testing only. |

0 commit comments

Comments
 (0)